diff --git a/.config/nextest.toml b/.config/nextest.toml index 0e8e4e1da3..20ab50745c 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -38,6 +38,14 @@ relative-to = "target" 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-gate` aggregate parses all 16 native Windows +# reports and requires exactly one successful execution of every runnable test. +[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 @@ -79,6 +87,26 @@ slow-timeout = { period = "180s", terminate-after = 1 } filter = 'test(test_eval_rejects_invalid_property_and_parameter_type_atoms)' slow-timeout = { period = "180s", terminate-after = 1 } +# Every `codegen::eval` fixture drives the eval bridge, which compiles AND LINKS a +# complete PHP program while the test is running -- the work the other entries in +# this file get an override for, except that here it is the whole module rather +# than a handful of cases. On an unloaded macOS AArch64 host they measure ~18-38s +# each; on a loaded native-Windows shard, where the link is MinGW's, that lands on +# either side of the 60s guard depending on the runner. +# +# The override is deliberately module-wide rather than a list of names. Naming the +# two cases that tripped on one run only moved the problem: the next run terminated +# two DIFFERENT eval fixtures on the same revision, because which of them crosses +# the line is a property of runner load, not of the individual test. None of them +# hangs -- each one that has been allowed to finish did. +[[profile.default.overrides]] +filter = 'test(/^codegen::eval::/)' +slow-timeout = { period = "180s", terminate-after = 1 } + +[[profile.ci.overrides]] +filter = 'test(/^codegen::eval::/)' +slow-timeout = { period = "180s", terminate-after = 1 } + [[profile.default.overrides]] filter = 'test(test_eval_declared_inherited_property_redeclaration_contracts)' slow-timeout = { period = "180s", terminate-after = 1 } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b17a08248..df9fb6e3a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,22 @@ env: -p elephc-image -p elephc-magician -p elephc-web + # Windows parity prebuild set: the bridges that cross-build cleanly for + # x86_64-pc-windows-gnu. elephc-phar is pure-Rust (bzip2 backed by + # libbz2-rs-sys, zlib backed by miniz_oxide -- no system libbz2/zlib + # dependency), so it needs no C toolchain support beyond MinGW. elephc-tls's + # `ring` crypto backend also builds cleanly under the provisioned MinGW CC; + # its fd-attachment surface uses a full-width Winsock SOCKET and duplicates + # it through `TcpStream::try_clone` on Windows. Building this set remains a + # structural bridge check; native behavior is covered separately below. + BRIDGE_CRATES_WINDOWS: >- + -p elephc-image + -p elephc-pdo + -p elephc-crypto + -p elephc-tz + -p elephc-phar + -p elephc-tls + -p elephc-web jobs: # Compile each platform once, then let that platform's tests start as soon as @@ -397,6 +413,15 @@ jobs: --no-fail-fast --retries 1 --flaky-result pass \ -j 1 + # Ordinary codegen fixtures are dominated by waiting on the assembler, the + # linker, and the compiled program, not by CPU in the test process itself, so + # two nextest workers per shard cut shard wall time without adding test runs -- + # the same trade the eval matrix below already makes. Each fixture builds in + # its own pid/tid-keyed temp directory (tests/codegen/support/projects.rs), + # binds ephemeral ports, and shares the runtime object cache through an atomic + # rename (src/runtime_cache.rs), so concurrent fixtures cannot collide. Median + # per-test time is ~0.8s against the profile's 60s slow-timeout, leaving ample + # headroom for the added contention. codegen-tests-macos-aarch64: name: Codegen Tests (macos-aarch64 ${{ matrix.shard }}/16) needs: build-archive-macos-aarch64 @@ -432,7 +457,7 @@ jobs: -E 'binary(codegen_tests) and not test(~codegen::eval)' \ --partition hash:${{ matrix.shard }}/16 \ --no-fail-fast --retries 1 --flaky-result pass \ - -j 1 + -j 2 codegen-tests-linux-x86_64: name: Codegen Tests (linux-x86_64 ${{ matrix.shard }}/16) @@ -468,7 +493,7 @@ jobs: -E 'binary(codegen_tests) and not test(~codegen::eval)' \ --partition hash:${{ matrix.shard }}/16 \ --no-fail-fast --retries 1 --flaky-result pass \ - -j 1 + -j 2 codegen-tests-linux-aarch64: name: Codegen Tests (linux-aarch64 ${{ matrix.shard }}/16) @@ -504,7 +529,7 @@ jobs: -E 'binary(codegen_tests) and not test(~codegen::eval)' \ --partition hash:${{ matrix.shard }}/16 \ --no-fail-fast --retries 1 --flaky-result pass \ - -j 1 + -j 2 # Eval integration tests compile, link, and run full programs while exercising # eval/magician paths, so keep them out of the ordinary codegen shards. Two @@ -667,6 +692,9 @@ jobs: # target/debug/examples/gen_builtins), so it must be built first. run: cargo build --example gen_builtins + - name: Test the builtin docs pipeline + run: python3 scripts/tests/test_builtin_docs.py + - 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 @@ -705,8 +733,15 @@ jobs: - codegen-tests-macos-aarch64 - codegen-tests-linux-x86_64 - codegen-tests-linux-aarch64 + - eval-codegen-tests-macos-aarch64 + - eval-codegen-tests-linux-x86_64 + - eval-codegen-tests-linux-aarch64 - image-api-sync - builtins-docs-sync + - windows-pe-cross-compile + - windows-pe-llvm-lld + - windows-bridge-native-build + - windows-codegen-gate if: always() steps: - name: Verify test jobs @@ -723,8 +758,15 @@ jobs: 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.eval-codegen-tests-macos-aarch64.result }}" = "success" + test "${{ needs.eval-codegen-tests-linux-x86_64.result }}" = "success" + test "${{ needs.eval-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-pe-llvm-lld.result }}" = "success" + test "${{ needs.windows-bridge-native-build.result }}" = "success" + test "${{ needs.windows-codegen-gate.result }}" = "success" benchmark: name: Benchmark Suite @@ -765,3 +807,580 @@ jobs: benchmark-results.md magician-benchmark-results.json magician-benchmark-results.md + + windows-pe-cross-compile: + name: Windows PE Cross-Compile Tests + runs-on: ubuntu-24.04 + timeout-minutes: 45 + env: + # Silence Wine's diagnostic chatter so it never pollutes captured stdout/stderr. + WINEDEBUG: -all + # This job installs MinGW-w64 and Wine on purpose, so a `windows_pe` fixture + # that cannot find either must fail rather than skip: every skip in that + # module reports as a pass, which would turn a degraded toolchain install + # into a green job that compiled and executed nothing. + ELEPHC_REQUIRE_WINDOWS_TOOLCHAIN: "1" + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Add x86_64-pc-windows-gnu Rust target + run: rustup target add x86_64-pc-windows-gnu + + - 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: Cross-build Windows bridge archives + env: + CC_x86_64_pc_windows_gnu: x86_64-w64-mingw32-gcc + AR_x86_64_pc_windows_gnu: x86_64-w64-mingw32-ar + RANLIB_x86_64_pc_windows_gnu: x86_64-w64-mingw32-ranlib + run: cargo build $BRIDGE_CRATES_WINDOWS --target x86_64-pc-windows-gnu + + - name: Verify PE/COFF bridge archive exports + run: | + python3 scripts/check_bridge_exports.py \ + --target x86_64-pc-windows-gnu \ + --nm x86_64-w64-mingw32-nm + + - name: Run Windows PE, web export, and Wine HTTP 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" + + # Exercise the opt-in LLVM path independently from the GNU/MinGW job above. + # This stays focused: it proves PE assembly, linking, Wine execution, transport + # visibility, and timeout metadata without duplicating the native 16-way shard set. + windows-pe-llvm-lld: + name: Windows PE LLVM/LLD Tests + runs-on: ubuntu-24.04 + timeout-minutes: 45 + env: + # Silence Wine's diagnostic chatter so it never pollutes captured stdout/stderr. + WINEDEBUG: -all + # As in windows-pe-cross-compile: MinGW-w64, LLVM/LLD, and Wine are all + # installed deliberately here, so a missing one must fail the job instead of + # silently downgrading these three regressions to vacuous passes. + ELEPHC_REQUIRE_WINDOWS_TOOLCHAIN: "1" + 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-llvm-${{ hashFiles('Cargo.lock') }} + restore-keys: | + rust-windows-pe-llvm- + + - name: Install LLVM, MinGW-w64, and inspection tools + run: | + sudo apt-get update + sudo apt-get install -y \ + binutils-mingw-w64-x86-64 \ + clang \ + gcc-mingw-w64-x86-64 \ + lld \ + file + + - name: Configure LLVM/LLD MinGW target + shell: bash + run: | + set -euo pipefail + sysroot="$(x86_64-w64-mingw32-gcc -print-sysroot)" + if [[ -z "$sysroot" || ! -d "$sysroot" ]]; then + for candidate in /usr/x86_64-w64-mingw32 /usr/x86_64-w64-mingw32/sys-root/mingw; do + if [[ -d "$candidate" ]]; then + sysroot="$candidate" + break + fi + done + fi + test -n "$sysroot" + test -d "$sysroot" + clang_bin="$(command -v clang)" + lld_bin="$(command -v ld.lld)" + gcc_bin="$(command -v x86_64-w64-mingw32-gcc)" + "$clang_bin" --version + "$lld_bin" --version + { + echo "ELEPHC_WINDOWS_TOOLCHAIN=llvm" + echo "ELEPHC_WINDOWS_CLANG=$clang_bin" + echo "ELEPHC_WINDOWS_LLD=$lld_bin" + echo "ELEPHC_WINDOWS_SYSROOT=$sysroot" + echo "ELEPHC_WINDOWS_GCC=$gcc_bin" + } >> "$GITHUB_ENV" + + - 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; `wine` dispatches to it for x86_64 PE. + 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: Compile and execute an LLVM/LLD PE smoke test under Wine + shell: bash + run: | + set -euo pipefail + echo ' /tmp/hello-llvm.php + cargo run -- --target windows-x86_64 /tmp/hello-llvm.php + file /tmp/hello-llvm.exe | grep -q "PE32+ executable (console) x86-64" + WINE_BIN=wine64 + command -v wine64 >/dev/null 2>&1 || WINE_BIN=wine + "$WINE_BIN" /tmp/hello-llvm.exe > /tmp/hello-llvm.out + grep -q "Hello from LLVM/LLD Windows!" /tmp/hello-llvm.out + + - name: Verify LLVM/LLD PE hardening and unwind metadata + run: cargo test --test codegen_tests -- test_windows_pe_hardening_and_generated_unwind_metadata --nocapture + + - name: Run LLVM/LLD Wine transport visibility regression + run: cargo test --test codegen_tests -- test_windows_does_not_expose_unix_socket_transports --nocapture + + - name: Run LLVM/LLD Wine timeout metadata regression + run: cargo test --test codegen_tests -- test_windows_stream_timeout_sets_metadata_without_eof --nocapture + + # Keep bridge/toolchain coverage separate from the strict native codegen + # shards below so archive/export regressions have a focused failure signal. + windows-bridge-native-build: + name: Windows Native PE & Bridge Tests + runs-on: windows-2025 + timeout-minutes: 90 + env: + # A native Windows host needs no Wine, but it still needs the MSYS2 MinGW + # toolchain this job installs. Refuse the skip path so a broken MSYS2 setup + # fails here instead of reporting the whole PE suite as passed. + ELEPHC_REQUIRE_WINDOWS_TOOLCHAIN: "1" + steps: + - uses: actions/checkout@v4 + + - name: Exclude the build and scratch trees from Defender + # Same rationale as the native codegen shards: this job assembles, links, + # and runs the whole PE suite, so real-time scanning taxes every fixture. + shell: pwsh + continue-on-error: true + run: | + foreach ($path in @($env:GITHUB_WORKSPACE, $env:RUNNER_TEMP, $env:TEMP, + (Join-Path $env:USERPROFILE '.cargo'), + (Join-Path $env:USERPROFILE '.rustup'))) { + if ($path) { Add-MpPreference -ExclusionPath $path -ErrorAction SilentlyContinue } + } + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: llvm-tools-preview + + - name: Install the native MinGW toolchain and libraries + id: msys2_bridge + uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + update: true + install: >- + mingw-w64-x86_64-binutils + mingw-w64-x86_64-bzip2 + mingw-w64-x86_64-gcc + mingw-w64-x86_64-libiconv + mingw-w64-x86_64-pcre2 + mingw-w64-x86_64-zlib + + - name: Cache Rust build state + uses: actions/cache@v4 + with: + path: | + ~/.cargo/git + ~/.cargo/registry + target + key: rust-windows-native-bridges-${{ hashFiles('Cargo.lock') }} + restore-keys: | + rust-windows-native-bridges- + + - name: Build bridge staticlibs with the native MSVC toolchain + shell: bash + run: | + cargo build -p elephc + cargo build $BRIDGE_CRATES_WINDOWS + + - name: Verify and expose GNU MinGW tools + shell: pwsh + run: | + $root = '${{ steps.msys2_bridge.outputs.msys2-location }}' + $binDir = Join-Path $root 'mingw64\bin' + foreach ($tool in @("gcc", "as", "ar", "ranlib", "nm", "objdump")) { + $target = Join-Path $binDir "x86_64-w64-mingw32-$tool.exe" + if (-not (Test-Path -LiteralPath $target)) { + $source = Join-Path $binDir "$tool.exe" + if (-not (Test-Path -LiteralPath $source)) { + throw "MSYS2 MinGW package setup did not provide $source" + } + New-Item -ItemType HardLink -Path $target -Target $source | Out-Null + } + & $target --version | Select-Object -First 1 + } + Add-Content -LiteralPath $env:GITHUB_PATH -Value $binDir + Add-Content -LiteralPath $env:GITHUB_ENV ` + -Value "ELEPHC_MINGW_SYSROOT=$(Join-Path $root 'mingw64')" + + - name: Cross-build GNU/COFF bridge archives for generated programs + shell: bash + env: + CC_x86_64_pc_windows_gnu: x86_64-w64-mingw32-gcc + AR_x86_64_pc_windows_gnu: x86_64-w64-mingw32-ar + RANLIB_x86_64_pc_windows_gnu: x86_64-w64-mingw32-ranlib + run: | + rustup target add x86_64-pc-windows-gnu + x86_64-w64-mingw32-gcc --version + cargo build $BRIDGE_CRATES_WINDOWS --target x86_64-pc-windows-gnu + + - name: Execute the complete PE suite, including native web HTTP probes + shell: bash + run: cargo test --test codegen_tests windows_pe -- --nocapture + + - name: Run bridge unit tests with a Unicode temporary path + shell: bash + run: | + PROBE_TEMP="$RUNNER_TEMP/elephc space été" + mkdir -p "$PROBE_TEMP" + TMP="$PROBE_TEMP" TEMP="$PROBE_TEMP" cargo test $BRIDGE_CRATES_WINDOWS + + - name: Verify native COFF bridge archive exports + shell: bash + run: | + HOST="$(rustc -vV | sed -n 's/^host: //p')" + LLVM_NM="$(rustc --print sysroot)/lib/rustlib/$HOST/bin/llvm-nm" + test -x "$LLVM_NM.exe" && LLVM_NM="$LLVM_NM.exe" + python scripts/check_bridge_exports.py --nm "$LLVM_NM" + + - name: Run bridge export verifier structural tests + shell: bash + run: python scripts/tests/test_check_bridge_exports.py + + # Run every runnable codegen fixture directly on a native Windows host. The + # shards are strict: nextest itself is the gate, so no failure baseline or + # allow-list can turn a failing fixture green. JUnit artifacts are retained + # solely to prove that the 8 hash partitions cover the runnable inventory + # exactly once. + # + # Eight shards of two workers, not sixteen of one. The account's ceiling is 20 + # concurrent jobs across the whole run; sixteen single-worker Windows shards + # claimed 16 of those 20 slots for the first ~38 minutes and starved the macOS + # lane, which then ran alone -- 5 jobs against 15 idle slots -- for the final + # ~27 minutes. Halving the shard count at double the workers keeps the same + # total test work and roughly the same per-shard duration while freeing eight + # slots for the rest of the matrix from the start of the run. + windows-codegen-native: + name: Windows Native Codegen (${{ matrix.shard }}/8) + runs-on: windows-2025 + # Each shard now carries twice the fixtures. The second worker is expected to + # absorb that, but the cap must not assume it does: at the measured 16-shard + # rate (~32 min) a shard whose second worker bought nothing would need ~67 + # min, which the previous 90 min cap only just cleared. Per-test hangs are + # already caught by the profile's 60s slow-timeout, so this outer cap only + # needs to be comfortably clear of the worst plausible honest run. + timeout-minutes: 120 + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3, 4, 5, 6, 7, 8] + env: + ELEPHC_TEST_TARGET: windows-x86_64 + # This job installs MinGW-w64 on purpose, so a fixture that finds no + # toolchain must fail rather than skip. Without this, a shard that lost + # `x86_64-w64-mingw32-gcc` from PATH would exit 0 out of every fixture and + # still hand `windows-codegen-gate` a complete, failure-free JUnit report -- + # the gate matches testcase names, not assertions, so it would print OK for + # a run that executed nothing. See `ensure_windows_runnable_or_skip`. + ELEPHC_REQUIRE_WINDOWS_TOOLCHAIN: "1" + steps: + - uses: actions/checkout@v4 + + - name: Exclude the build and scratch trees from Defender + # Every codegen fixture writes an .s file, runs the MinGW assembler and + # linker over it, and executes a brand-new .exe. Real-time scanning meters + # each of those, which is why a native Windows shard costs ~4s per fixture + # against ~0.8s on Linux for the same work. The runner is an ephemeral, + # single-use VM, so scanning its own build output buys nothing. Never fail + # the job over this: the exclusion is an optimization, not a requirement. + shell: pwsh + continue-on-error: true + run: | + foreach ($path in @($env:GITHUB_WORKSPACE, $env:RUNNER_TEMP, $env:TEMP, + (Join-Path $env:USERPROFILE '.cargo'), + (Join-Path $env:USERPROFILE '.rustup'))) { + if ($path) { Add-MpPreference -ExclusionPath $path -ErrorAction SilentlyContinue } + } + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Add the MinGW Rust target + run: rustup target add x86_64-pc-windows-gnu + + # GitHub's Windows curl uses Schannel. Its normal CRL distribution-point + # lookup intermittently fails on hosted runners with + # CRYPT_E_REVOCATION_OFFLINE before nextest is downloaded. Keep TLS, + # hostname, chain, and actual-revocation checks enabled; only make an + # unavailable CRL endpoint non-fatal for this installation action. + - name: Configure curl for the cargo-nextest download + id: nextest_curl + shell: pwsh + run: | + $curlHome = Join-Path $env:RUNNER_TEMP 'elephc-nextest-curl' + New-Item -ItemType Directory -Path $curlHome -Force | Out-Null + Set-Content -LiteralPath (Join-Path $curlHome '.curlrc') ` + -Value 'ssl-revoke-best-effort' -NoNewline -Encoding ascii + "home=$curlHome" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 + + - name: Install cargo-nextest + uses: taiki-e/install-action@nextest + env: + CURL_HOME: ${{ steps.nextest_curl.outputs.home }} + + - name: Install the native MinGW toolchain and libraries + id: msys2_codegen + uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + update: true + install: >- + mingw-w64-x86_64-binutils + mingw-w64-x86_64-bzip2 + mingw-w64-x86_64-gcc + mingw-w64-x86_64-libiconv + mingw-w64-x86_64-pcre2 + mingw-w64-x86_64-zlib + + - name: Verify and expose GNU MinGW tools + shell: pwsh + run: | + $root = '${{ steps.msys2_codegen.outputs.msys2-location }}' + $binDir = Join-Path $root 'mingw64\bin' + foreach ($tool in @("gcc", "as", "ar", "ranlib", "nm", "objdump")) { + $target = Join-Path $binDir "x86_64-w64-mingw32-$tool.exe" + if (-not (Test-Path -LiteralPath $target)) { + $source = Join-Path $binDir "$tool.exe" + if (-not (Test-Path -LiteralPath $source)) { + throw "MSYS2 MinGW package setup did not provide $source" + } + New-Item -ItemType HardLink -Path $target -Target $source | Out-Null + } + & $target --version | Select-Object -First 1 + } + Add-Content -LiteralPath $env:GITHUB_PATH -Value $binDir + Add-Content -LiteralPath $env:GITHUB_ENV ` + -Value "ELEPHC_MINGW_SYSROOT=$(Join-Path $root 'mingw64')" + + - name: Cache Rust build state + uses: actions/cache@v4 + with: + path: | + ~/.cargo/git + ~/.cargo/registry + target + key: rust-windows-native-codegen-${{ hashFiles('Cargo.lock') }} + restore-keys: | + rust-windows-native-codegen- + + - name: Build compiler and GNU/COFF bridge archives + shell: bash + env: + CC_x86_64_pc_windows_gnu: x86_64-w64-mingw32-gcc + AR_x86_64_pc_windows_gnu: x86_64-w64-mingw32-ar + RANLIB_x86_64_pc_windows_gnu: x86_64-w64-mingw32-ranlib + run: | + set -euo pipefail + cargo build -p elephc + cargo build $BRIDGE_CRATES_WINDOWS -p elephc-magician \ + --target x86_64-pc-windows-gnu + mkdir -p target/debug + cp target/x86_64-pc-windows-gnu/debug/libelephc_*.a target/debug/ + + - name: Run native Windows codegen shard + shell: bash + run: | + cargo nextest run --profile ci --test codegen_tests \ + --partition hash:${{ matrix.shard }}/8 \ + --no-fail-fast \ + -j 2 + + - name: Verify shard JUnit report + if: always() + shell: bash + run: | + test -s target/nextest/ci/junit.xml + grep -q ' target/nextest/ci/nextest-list.json + + - name: Upload shard JUnit report + if: always() + uses: actions/upload-artifact@v4 + with: + name: windows-codegen-junit-${{ matrix.shard }} + path: target/nextest/ci/junit.xml + if-no-files-found: error + retention-days: 7 + + - name: Upload runnable codegen inventory + if: always() && matrix.shard == 1 + uses: actions/upload-artifact@v4 + with: + name: windows-codegen-runnable-inventory + path: target/nextest/ci/nextest-list.json + if-no-files-found: error + retention-days: 7 + + # Aggregate the native reports independently of each matrix job's exit status. + # This proves both strict success and exact one-time coverage, so a missing or + # truncated shard cannot masquerade as a green Windows result. + windows-codegen-gate: + name: Windows Native Codegen Gate + runs-on: ubuntu-latest + needs: + - windows-codegen-native + if: always() + steps: + - uses: actions/checkout@v4 + + - name: Download shard JUnit reports + uses: actions/download-artifact@v4 + with: + pattern: windows-codegen-junit-* + path: artifacts/junit + + - name: Download runnable codegen inventory + uses: actions/download-artifact@v4 + with: + name: windows-codegen-runnable-inventory + path: artifacts/inventory + + - name: Test the coverage verifier itself + # This job's verdict is only as trustworthy as the script producing it, and + # the script's own regression tests -- including the case proving + # `--require-success` actually rejects a failing testcase -- were never run + # anywhere in CI. Run them before trusting the verifier, the same way + # `test_check_bridge_exports.py` guards its sibling. + run: python3 scripts/tests/test_gen_windows_codegen_allowlist.py + + - name: Verify complete Windows codegen coverage + shell: bash + run: | + junit_args=() + while IFS= read -r report; do + junit_args+=(--junit "$report") + done < <(find artifacts/junit -name junit.xml -type f | sort) + python3 scripts/gen_windows_codegen_allowlist.py verify-complete \ + --list-json artifacts/inventory/nextest-list.json \ + "${junit_args[@]}" \ + --expected-junit-count 8 \ + --require-success + + - name: Verify native shard jobs + run: | + echo "windows-codegen-native result: ${{ needs.windows-codegen-native.result }}" + test "${{ needs.windows-codegen-native.result }}" = "success" diff --git a/AGENTS.md b/AGENTS.md index 5c69741742..144491a1d2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,7 @@ Before contributing, read `CONTRIBUTING.md` in full. It holds the complete step- ## Supported target policy -All supported targets are first-class targets. The supported target matrix is currently `macos-aarch64`, `linux-aarch64`, and `linux-x86_64`. +All supported targets are first-class targets. The supported target matrix is currently `macos-aarch64`, `linux-aarch64`, and `linux-x86_64`. `windows-x86_64` is an experimental PE/MinGW target with dedicated native/Wine execution and a strict 8-shard native codegen gate. Promotion requires one same-revision green native gate plus the target-policy review documented in `docs/compiling/targets.md`; the legacy Wine allow-list files are historical snapshots, not acceptance baselines. 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. @@ -424,6 +424,49 @@ Adding or updating function docblocks must not change code behavior. Do not alte - **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 +### Windows x86_64 (MSx64) ABI reference + +Generated Windows functions and C-facing calls use the Microsoft x64 ABI. Much +of the handwritten x86_64 runtime retains its internal SysV-shaped convention, +so calls crossing that boundary use per-symbol shims under +`src/codegen_support/runtime/win32/`. Rust bridge calls and published bridge +function pointers must go through the centralized adapters in +`src/codegen_support/emit.rs`; do not call them as if they were internal runtime +symbols. +When writing or reviewing a Win32 shim, hold these rules: + +- **Integer args**: `rcx`, `rdx`, `r8`, `r9`, then the stack. Every call reserves a + 32-byte **shadow space** the callee owns; the 5th and later integer args go at + `[rsp+32]`, `[rsp+40]`, … (above the shadow), never in more registers. +- **Callee-saved**: `rbx`, `rbp`, `rdi`, `rsi`, `r12`–`r15`. Note `rsi`/`rdi` are + **non-volatile** on MSx64 (unlike SysV), so a shim can stage the incoming SysV + path/buffer pointer in `rsi`/`rdi` and rely on it surviving every nested Win32 call. +- **Stack alignment**: a shim is entered at `rsp ≡ 8 (mod 16)` (the `call` pushed the + return address). Re-align with `sub rsp, N` where `N ≡ 8 (mod 16)` — i.e. **40 or 56**, + not 32 — so `rsp ≡ 0 (mod 16)` at the nested `call`. The unit test + `test_stack_alignment_16_bytes` enforces this; the only legitimate `sub rsp, 32` is + the exit shim, which first forces alignment with `and rsp, -16`. +- **Struct / out-param layout is UPWARD (C layout)**: the pointer you pass to an API is + the struct's **lowest** address, and a field at byte offset `F` lives at `base + F` — + higher offset ⇒ higher address ⇒ *less-negative* `rbp`/`rsp` offset. Never lay a struct + downward (`base − F`); a downward layout is invisible to the macOS/Linux tests and only + fails under wine. Canonical reference: the `pselect6` fd_set shim (`fd_count@base+0`, + `fd_array@base+8`). Zero a struct fully before filling it. This class of bug bit us on + the proc_open `STARTUPINFOA`/`PROCESS_INFORMATION` layout and on the `statfs`/`utsname`/ + `FILETIME`/`BY_HANDLE_FILE_INFORMATION` fills. +- **Status-convention translation**: many Win32 APIs return a `BOOL` (nonzero = success). + A shim standing in for a POSIX C symbol whose consumer tests `== 0` for success + (`link`, `rename`, …) **must** translate the `BOOL` to POSIX (`0` = success, + `-1` = failure) inside the shim, or success and failure are reported inverted. Mirror + the `link`/`rename` shims: `test eax, eax; jz .Lfail; xor rax, rax` / `.Lfail: mov rax, -1`. +- **32-bit int-status sign extension (Class-3)**: a shim returning a 32-bit C `int` status + that a consumer sign-tests must `cdqe` before returning, so a negative status is not read + as a large positive `rax`. +- **Adding a shim**: declare the Win32 import in `WIN32_IMPORTS`, add the `emit_shim_*` and + its call in `emit_win32_shims`, and keep non-Windows emitters byte-identical (only the + Windows arm and `WIN32_IMPORTS` change). `ntdll`-only APIs (e.g. `RtlGetVersion`) are + **not** in the link set — do not import them; use a documented fallback instead. + ### Assembly comment policy Every `emitter.instruction(...)` call must have an inline `//` comment aligned to diff --git a/CHANGELOG.md b/CHANGELOG.md index 1614cdd2ba..1bc7a7add6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Releases are listed newest first. - Added a live per-phase spinner, cargo-style bridge-library "Linking" lines, colorized errors/warnings/success output, and a `--timings` report with per-phase percentages — all shown only on an interactive terminal and skippable with the new `--quiet`/`-q` flag, which reproduces the previous plain output exactly. - Added a comprehensive `-h`/`--help` flag listing every CLI option by category, and made every parameter error (unknown flag, invalid value, missing source file) print a consistent `error: ` plus short usage and a `--help` hint, replacing the previous inconsistent mix of a bare message or a single dense usage line. - Fixed `implode()` over an indexed array held in a boxed `mixed` cell leaking one heap block per string element (issue #601). Each boxed element is stringified through the persisting mixed-string cast, which allocates an owned copy for string payloads; implode now releases that copy after copying its bytes into the result buffer, on every supported target. Integer, float, and bool elements (which stringify into shared scratch storage) and typed non-mixed string arrays (whose elements are borrowed) are unaffected, and the joined output remains valid. +- Extended `--web` to the experimental Windows x86_64 PE backend. Unix keeps its prefork supervisor, while Windows uses a single-process current-thread event loop with Ctrl-C shutdown, serialized PHP execution, watchdog termination, and clean `--max-requests` exit. Dedicated PE tests verify the web bridge exports and real `200`/`400` HTTP responses under Wine and native Windows. ## [0.26.2] - Added experimental PHP `eval()` support across macOS ARM64, Linux ARM64, and Linux x86_64. Eligible literal fragments are parsed at compile time and lowered to native EIR, including direct or scope-backed caller-local synchronization; dynamic strings and unsupported literal shapes fall back to the optional statically linked `elephc-magician` EvalIR interpreter. Within the supported eval subset, the fallback preserves caller/global scope updates, dynamic functions/classes/constants, callables, reflection, builtins, exceptions, ownership/COW behavior, and PHP-visible diagnostics without requiring PHP or the Zend Engine. Bridge linking is automatic when required and can be forced with `--with-eval`; generated builtin documentation now reports AOT and eval availability separately. @@ -75,7 +76,7 @@ Releases are listed newest first. - Deprecated `${var}` / `${expr}` string interpolation is now accepted by the lexer (issue #340), matching PHP 8.x's deprecated-but-working behavior. - `var_dump()` is now variadic (issue #389): each argument is dumped independently in source order. `print_r()` gains the `$return` flag — `print_r($v, true)` returns the rendered string instead of echoing, including when the flag is only known at runtime (`string|bool` boxed result); captures are truncated at the 64 KiB buffer cap. - Reference aliases to indexed-array elements (issue #331): `$b =& $a[0]` binds a local to the element's storage with write-through in both directions, on every supported target. Associative arrays and out-of-range autovivification are documented limits. -- Windows groundwork (issue #379): `windows-x86_64` is parsed as a target and every platform dispatch has an explicit "not yet supported" diagnostic instead of an exhaustiveness gap. +- Windows groundwork (issue #379): `windows-x86_64` target parsing and explicit platform dispatch diagnostics landed as the foundation later extended by the experimental PE32+/MinGW backend described below. - Source maps v2: `--source-map` now writes a versioned machine-readable schema (`format: "elephc-source-map"`, `version: 2`) with function ranges (PHP name, entry symbol, assembly line range, synthetic flag for compiler-generated bodies), assembly labels attributed to their owning function and EIR basic block, instruction mappings tagged with the originating EIR opcode, expression end positions, and optimization provenance (`const_fold`/`licm`), plus a PHP-line → assembly-range inverse index and a `source_sha256` staleness checksum. The schema contract for external tooling is documented in `docs/compiling/source-maps.md`; the flat v1 `entries` format is superseded. - New `--debug-info` flag: embeds DWARF debug information in the generated assembly — a `.file`/`.loc` line table plus a compile unit with one `DW_TAG_subprogram` per PHP function, derived from the same source markers as `--source-map` — so lldb/gdb breakpoints (`b file.php:3`) and profiler samples resolve to PHP source lines without custom tooling. On macOS the pipeline runs `dsymutil` to produce a `.dSYM` next to the binary (keeping the object file as a fallback when that fails); on Linux the line tables link directly into the binary. - Expression spans now carry end positions: the lexer records token extents and the parser widens binary, assignment, and call spans through their last token, keeping the start anchored so diagnostics are unchanged. @@ -89,6 +90,8 @@ Releases are listed newest first. `true` uses strict type-identical membership. - Added `mb_ereg_match()`: a PCRE2-backed, start-anchored mbregex builtin with the optional `$options` argument and support for `i` case-insensitive matching. +- Add the initial experimental Windows x86_64 (PE32+) cross-compilation target (`--target windows-x86_64`, alias `x86_64-pc-windows-gnu`): at introduction it produced a GNU/MinGW-ABI `.exe` (`.dll` for `--emit cdylib`) via `x86_64-w64-mingw32-gcc`, while runtime shims and execution coverage were still catching up. The Unreleased entries above describe the subsequently expanded PE, bridge, eval, web, native-Windows, Wine, and optional LLVM/LLD coverage; the target remains experimental until the same revision passes the strict native gate and target-policy review. +- 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. diff --git a/Cargo.lock b/Cargo.lock index 9c4e5a475d..ab7ae6859b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,6 +171,15 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "borsh" version = "1.6.1" @@ -395,6 +404,16 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -518,6 +537,17 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "ctrlc" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +dependencies = [ + "dispatch2", + "nix", + "windows-sys 0.61.2", +] + [[package]] name = "ctutils" version = "0.4.2" @@ -572,6 +602,18 @@ dependencies = [ "ctutils", ] +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.0", + "block2", + "libc", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.6" @@ -648,6 +690,7 @@ dependencies = [ "elephc-crypto", "elephc-phar", "flate2", + "getrandom 0.2.17", "inventory", "libc", "unicode-segmentation", @@ -673,6 +716,7 @@ dependencies = [ "bzip2", "bzip2-rs", "flate2", + "getrandom 0.2.17", "md-5 0.10.6", "rsa", "sha1", @@ -685,6 +729,7 @@ version = "0.1.0" dependencies = [ "libc", "rustls", + "rustls-native-certs", "rustls-pemfile", "webpki-roots 0.26.11", ] @@ -697,6 +742,7 @@ version = "0.1.0" name = "elephc-web" version = "0.1.0" dependencies = [ + "ctrlc", "flate2", "http", "http-body-util", @@ -705,6 +751,7 @@ dependencies = [ "libc", "socket2 0.5.10", "tokio", + "windows-sys 0.61.2", ] [[package]] @@ -1514,6 +1561,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -1586,6 +1645,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + [[package]] name = "objc2-core-foundation" version = "0.3.2" @@ -1595,6 +1663,12 @@ dependencies = [ "bitflags 2.13.0", ] +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + [[package]] name = "objc2-system-configuration" version = "0.3.2" @@ -1619,6 +1693,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "parking_lot" version = "0.12.5" @@ -2119,6 +2199,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "2.2.0" @@ -2160,6 +2252,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ece8e78b2f38ec51c51f5d475df0a7187ba5111b2a28bdc761ee05b075d40a71" +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2172,6 +2273,29 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.28" diff --git a/README.md b/README.md index ab43d9a6b3..1053073430 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,11 @@

- 3 native targets · no Zend Engine · no external PHP runtime · single standalone binary + 4 native targets · no Zend Engine · no external PHP runtime · single standalone binary

- A PHP-to-native compiler that takes a subset of PHP and compiles it directly to native assembly, producing standalone binaries for macOS ARM64, Linux ARM64, and Linux x86_64. Ordinary source is AOT-compiled with no opcode fallback; experimental eval() can embed an optional interpreter bridge when runtime parsing is required. + 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, Linux x86_64, and an experimental Windows x86_64 cross-compilation target. Ordinary source is AOT-compiled with no opcode fallback; experimental eval() can embed an optional interpreter bridge when runtime parsing is required.

@@ -86,7 +86,7 @@ I made the project as modular as possible. Every function has its own codegen fi ## What you can expect -You can write PHP using the constructs documented in the [docs](docs/). Classes with single inheritance, interfaces, `instanceof`, nullsafe access (`?->`), abstract classes, final classes, methods and typed/static properties, PHP-style static property redeclarations, constructor property promotion, traits, constructors, instance/static methods, case-insensitive PHP symbol lookup for functions/classes/methods, `self::` / `parent::` / `static::` with late static binding, `readonly` properties and classes, enums, PHP 8 attributes on declarations, named arguments, first-class callables, typed function and method parameters and returns, `try` / `catch` / `finally` / `throw`, visibility modifiers, union and nullable types, copy-on-write arrays, associative arrays with PHP insertion order and integer/numeric-string key normalization, array union with `+`, closures, generator functions and generator closures with `yield` / `yield from`, namespaces, includes, compile-time Composer/SPL autoloading, class/introspection helpers, `PDO` database access (`PDO` / `PDOStatement` / `PDOException`) with SQLite, PostgreSQL, and MySQL/MariaDB drivers, image creation and manipulation (GD raster I/O, drawing, transforms/filters, Exif/IPTC metadata, and the `Imagick`/`Gmagick`/Cairo object APIs) on a pure-Rust codec/raster bridge, and PHP 8.1-style `Fiber` coroutines on macOS ARM64, Linux ARM64, and Linux x86_64. +You can write PHP using the constructs documented in the [docs](docs/). Classes with single inheritance, interfaces, `instanceof`, nullsafe access (`?->`), abstract classes, final classes, methods and typed/static properties, PHP-style static property redeclarations, constructor property promotion, traits, constructors, instance/static methods, case-insensitive PHP symbol lookup for functions/classes/methods, `self::` / `parent::` / `static::` with late static binding, `readonly` properties and classes, enums, PHP 8 attributes on declarations, named arguments, first-class callables, typed function and method parameters and returns, `try` / `catch` / `finally` / `throw`, visibility modifiers, union and nullable types, copy-on-write arrays, associative arrays with PHP insertion order and integer/numeric-string key normalization, array union with `+`, closures, generator functions and generator closures with `yield` / `yield from`, namespaces, includes, compile-time Composer/SPL autoloading, class/introspection helpers, `PDO` database access (`PDO` / `PDOStatement` / `PDOException`) with SQLite, PostgreSQL, and MySQL/MariaDB drivers, image creation and manipulation (GD raster I/O, drawing, transforms/filters, Exif/IPTC metadata, and the `Imagick`/`Gmagick`/Cairo object APIs) on a pure-Rust codec/raster bridge, and PHP 8.1-style `Fiber` coroutines on macOS ARM64, Linux ARM64, Linux x86_64, and the experimental Windows x86_64 backend. Experimental [`eval()` support](docs/php/eval.md) AOT-lowers eligible literal fragments and falls back to the optional, statically linked Magician interpreter for dynamic fragments. Runnable examples live in [`examples/eval/`](examples/eval/) and [`examples/eval-globals/`](examples/eval-globals/). @@ -206,11 +206,12 @@ elephc app.php --with-pdo --with-crypto elephc app.php --with-eval # 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; MinGW ABI/sysroot (GNU default, LLVM optional) -# Compile a standalone prefork HTTP server binary +# Compile a standalone HTTP server (Unix prefork; Windows event loop) elephc --web app.php ./app --listen 127.0.0.1:8080 ./app --listen 0.0.0.0:8080 --workers 4 @@ -326,7 +327,7 @@ The full list of supported constructs, operators, and control structures is in t - **OOP**: classes, abstract/final classes, typed/final/static properties and methods, PHP-style static property redeclarations, direct static array property writes, constructor property promotion, interfaces, `instanceof`, traits, enums, PHP 8 declaration attributes, limited attribute reflection (`ReflectionClass`/`ReflectionMethod`/`ReflectionProperty::getAttributes()`, `ReflectionAttribute::newInstance()`), `readonly`, static/instance methods, case-insensitive class/interface/trait and method lookup, `self::`/`parent::`/`static::`, `::class` reflection (including `$object::class` on object expressions, returning the receiver's runtime class), class constants including PHP 8.3 typed class constants (exposed via `ReflectionClassConstant::hasType()`/`getType()`), `new self()` / `new static()` / `new parent()`, magic methods (`__toString`, `__get`, `__set`) - **Functions**: case-insensitive user and built-in function calls, default parameters, variadic/spread, pass by reference, named arguments, global variables, static locals, first-class callables, closures, arrow functions, static closures (`static function () { }`, `static fn () => ...`) - **Generators**: generator functions and closures, `yield`, key/value yields, `yield from`, `Generator::send()`, `throw()`, `getReturn()`, and `foreach` over `Iterator` / `IteratorAggregate` -- **Fibers**: `Fiber`, `FiberError`, `Fiber::suspend()`, `Fiber::getCurrent()`, `start()`, `resume()`, `throw()`, `getReturn()`, state predicates, closure captures, guarded native stacks, and target-aware context switching on macOS ARM64, Linux ARM64, and Linux x86_64 +- **Fibers**: `Fiber`, `FiberError`, `Fiber::suspend()`, `Fiber::getCurrent()`, `start()`, `resume()`, `throw()`, `getReturn()`, state predicates, closure captures, guarded native stacks, and target-aware context switching on macOS ARM64, Linux ARM64, Linux x86_64, and the experimental Windows x86_64 backend - **Control flow**: if/elseif/else, while, do-while, for, foreach, switch, match, break/continue including multi-level depths, try/catch/finally/throw - **Statements and literals**: `const` / `define()` constants, `global` declarations, `static` locals (with or without an initializer), `print` expressions, list unpacking, PHP numeric literal forms, heredoc / nowdoc strings, `declare(strict_types=1)` and `declare(ticks=...)` directives (validated syntactically and treated as no-ops — elephc compiles an always-strict subset) - **Operators**: arithmetic, comparison, `instanceof`, logical, bitwise, ternary, null coalescing (`??`), PHP 8.5 pipe (`|>`), assignment expressions for local and stabilized non-local targets, null coalescing assignment (`??=`), error control (`@`), and compound assignments diff --git a/ROADMAP.md b/ROADMAP.md index fc82ea6175..8ea7e0102c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -957,6 +957,8 @@ and 0.x validation rather than by speculative pass work. - [ ] 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 - [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 diff --git a/crates/elephc-crypto/src/lib.rs b/crates/elephc-crypto/src/lib.rs index c18cc48dc6..f0a38118a9 100644 --- a/crates/elephc-crypto/src/lib.rs +++ b/crates/elephc-crypto/src/lib.rs @@ -107,17 +107,17 @@ impl HashCtx { } /// Finalizes the context into its raw digest, consuming it. - fn finalize(self) -> Vec { + fn finalize(self) -> Option> { match self { - HashCtx::Plain(s) => s.finalize_box(), + HashCtx::Plain(s) => Some(s.finalize_box()), HashCtx::Hmac { algo, opad_material, inner } => { let inner_digest = inner.finalize_box(); - // constructively unreachable: make() is a static table and this algo was accepted - // just above; note a panic here would abort across the extern "C" boundary. - let mut outer = make(&algo).expect("hmac algo was validated at init"); + // Keep the FFI boundary fallible even if the static algorithm table and + // HMAC block-size table accidentally drift out of sync in the future. + let mut outer = make(&algo)?; outer.update(&opad_material); outer.update(&inner_digest); - outer.finalize_box() + Some(outer.finalize_box()) } } } @@ -169,9 +169,11 @@ pub unsafe extern "C" fn elephc_crypto_init_hmac( }; let ipad: Vec = k.iter().map(|b| b ^ 0x36).collect(); let opad_material: Vec = k.iter().map(|b| b ^ 0x5c).collect(); - // constructively unreachable: make() is a static table and this algo was accepted - // just above; note a panic here would abort across the extern "C" boundary. - let mut inner = make(&name).expect("algo validated by block_key"); + // Treat registry drift as a normal bridge failure instead of panicking across C ABI. + let mut inner = match make(&name) { + Some(inner) => inner, + None => return std::ptr::null_mut(), + }; inner.update(&ipad); Box::into_raw(Box::new(HashCtx::Hmac { algo: name, opad_material, inner })) as *mut c_void } @@ -214,7 +216,10 @@ pub unsafe extern "C" fn elephc_crypto_final(ctx: *mut c_void, out_ptr: *mut u8) return -1; } let ctx = &*(ctx as *mut HashCtx); - let digest = ctx.clone_box().finalize(); + let digest = match ctx.clone_box().finalize() { + Some(digest) => digest, + None => return -1, + }; std::ptr::copy_nonoverlapping(digest.as_ptr(), out_ptr, digest.len()); digest.len() as isize } diff --git a/crates/elephc-image/src/cairo/context.rs b/crates/elephc-image/src/cairo/context.rs index 15a2367f60..f3b48c6205 100644 --- a/crates/elephc-image/src/cairo/context.rs +++ b/crates/elephc-image/src/cairo/context.rs @@ -416,9 +416,7 @@ pub extern "C" fn elephc_cairo_identity_matrix(ctx: i64) { #[no_mangle] pub extern "C" fn elephc_cairo_get_current_point_x(ctx: i64) -> i64 { ffi_guard(-1, move || { - contexts() - .lock() - .unwrap() + lock_recover(contexts()) .get(&ctx) .map_or(0, |c| (c.cur.x * 1000.0).round() as i64) }) @@ -428,9 +426,7 @@ pub extern "C" fn elephc_cairo_get_current_point_x(ctx: i64) -> i64 { #[no_mangle] pub extern "C" fn elephc_cairo_get_current_point_y(ctx: i64) -> i64 { ffi_guard(-1, move || { - contexts() - .lock() - .unwrap() + lock_recover(contexts()) .get(&ctx) .map_or(0, |c| (c.cur.y * 1000.0).round() as i64) }) diff --git a/crates/elephc-image/src/cairo/surface.rs b/crates/elephc-image/src/cairo/surface.rs index 76288d3830..e68eeff692 100644 --- a/crates/elephc-image/src/cairo/surface.rs +++ b/crates/elephc-image/src/cairo/surface.rs @@ -49,9 +49,7 @@ pub extern "C" fn elephc_cairo_surface_destroy(s: i64) { #[no_mangle] pub extern "C" fn elephc_cairo_surface_width(s: i64) -> i64 { ffi_guard(-1, move || { - surfaces() - .lock() - .unwrap() + lock_recover(surfaces()) .get(&s) .map_or(-1, |pm| pm.width() as i64) }) @@ -61,9 +59,7 @@ pub extern "C" fn elephc_cairo_surface_width(s: i64) -> i64 { #[no_mangle] pub extern "C" fn elephc_cairo_surface_height(s: i64) -> i64 { ffi_guard(-1, move || { - surfaces() - .lock() - .unwrap() + lock_recover(surfaces()) .get(&s) .map_or(-1, |pm| pm.height() as i64) }) diff --git a/crates/elephc-image/src/codec.rs b/crates/elephc-image/src/codec.rs index 632d625a76..7671dacb3a 100644 --- a/crates/elephc-image/src/codec.rs +++ b/crates/elephc-image/src/codec.rs @@ -128,9 +128,15 @@ pub extern "C" fn elephc_img_stage_ptr(len: i64) -> *mut u8 { if len <= 0 { return std::ptr::null_mut(); } + let Ok(len) = usize::try_from(len) else { + return std::ptr::null_mut(); + }; let mut guard = lock_recover(stage_cell()); guard.clear(); - guard.resize(len as usize, 0); + if guard.try_reserve_exact(len).is_err() { + return std::ptr::null_mut(); + } + guard.resize(len, 0); guard.as_mut_ptr() }) } diff --git a/crates/elephc-image/src/draw.rs b/crates/elephc-image/src/draw.rs index f0e1242e0b..e43ceee982 100644 --- a/crates/elephc-image/src/draw.rs +++ b/crates/elephc-image/src/draw.rs @@ -296,8 +296,12 @@ fn fill_polygon(img: &mut RgbaImage, blending: bool, pts: &[(i64, i64)], src: Rg if pts.len() < 3 { return; } - let ymin = pts.iter().map(|p| p.1).min().unwrap(); - let ymax = pts.iter().map(|p| p.1).max().unwrap(); + let Some(ymin) = pts.iter().map(|p| p.1).min() else { + return; + }; + let Some(ymax) = pts.iter().map(|p| p.1).max() else { + return; + }; let n = pts.len(); for y in ymin..=ymax { let mut xs: Vec = Vec::new(); diff --git a/crates/elephc-image/src/imagick.rs b/crates/elephc-image/src/imagick.rs index 6d986834ac..8ea8f617b8 100644 --- a/crates/elephc-image/src/imagick.rs +++ b/crates/elephc-image/src/imagick.rs @@ -274,9 +274,7 @@ pub extern "C" fn elephc_imagick_add_image(dst_wand: i64, src_wand: i64) -> i64 let Some(src_handle) = current_handle(src_wand) else { return -1; }; - let src_fmt = wands() - .lock() - .unwrap() + let src_fmt = lock_recover(wands()) .get(&src_wand) .map(|w| w.format) .unwrap_or(FMT_PNG); diff --git a/crates/elephc-image/src/lib.rs b/crates/elephc-image/src/lib.rs index 46aebf3368..9b21bd2519 100644 --- a/crates/elephc-image/src/lib.rs +++ b/crates/elephc-image/src/lib.rs @@ -285,3 +285,108 @@ pub(crate) fn pack_color(pixel: Rgba) -> i64 { let gd_alpha = ((255 - a as u32) * 127 / 255) as i64; (gd_alpha << 24) | ((r as i64) << 16) | ((g as i64) << 8) | b as i64 } + +#[cfg(test)] +mod tests { + use std::ffi::CString; + + use super::*; + + /// All statically enabled raster codecs encode and decode a small image + /// through the same staging-buffer ABI used by generated PHP programs. + #[test] + fn static_codec_round_trip_matrix() { + let handle = gd::elephc_img_create_truecolor(3, 2); + assert!(handle > 0); + gd::elephc_img_set_pixel(handle, 1, 1, 0x00_33_66_cc); + + for format in [FMT_PNG, FMT_JPEG, FMT_GIF, FMT_BMP, FMT_WEBP] { + assert_eq!(codec::elephc_img_encode(handle, format, 85), 0); + let len = codec::elephc_img_encoded_len(); + assert!(len > 0, "format {format} produced no bytes"); + let stage = codec::elephc_img_stage_ptr(len); + assert!(!stage.is_null()); + unsafe { + std::ptr::copy_nonoverlapping( + codec::elephc_img_encoded_ptr(), + stage, + len as usize, + ); + } + let decoded = codec::elephc_img_create_from_stage(len); + assert!(decoded > 0, "format {format} did not decode"); + assert_eq!(gd::elephc_img_sx(decoded), 3); + assert_eq!(gd::elephc_img_sy(decoded), 2); + gd::elephc_img_destroy(decoded); + codec::elephc_img_encoded_clear(); + } + gd::elephc_img_destroy(handle); + } + + /// File-backed PNG I/O preserves a non-ASCII path and image geometry, + /// exercising Rust's UTF-8-to-native-path conversion used on Windows. + #[test] + fn unicode_file_path_round_trip() { + let root = std::env::temp_dir().join(format!( + "elephc-image-Données-日本語-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir_all(&root).expect("create Unicode image directory"); + let path = root.join("résultat-東京.png"); + let c_path = CString::new(path.to_string_lossy().as_bytes()).expect("valid image path"); + + let handle = gd::elephc_img_create_truecolor(4, 3); + assert!(handle > 0); + assert_eq!( + unsafe { codec::elephc_img_write_file(handle, FMT_PNG, c_path.as_ptr(), -1) }, + 0 + ); + let decoded = unsafe { codec::elephc_img_create_from_file(c_path.as_ptr(), FMT_PNG) }; + assert!(decoded > 0); + assert_eq!((gd::elephc_img_sx(decoded), gd::elephc_img_sy(decoded)), (4, 3)); + + let tga_path = root.join("résultat-東京.tga"); + let c_tga = CString::new(tga_path.to_string_lossy().as_bytes()).expect("valid TGA path"); + assert_eq!( + unsafe { codec::elephc_img_write_file(handle, FMT_TGA, c_tga.as_ptr(), -1) }, + 0 + ); + let decoded_tga = + unsafe { codec::elephc_img_create_from_file(c_tga.as_ptr(), FMT_TGA) }; + assert!(decoded_tga > 0); + assert_eq!( + (gd::elephc_img_sx(decoded_tga), gd::elephc_img_sy(decoded_tga)), + (4, 3) + ); + + gd::elephc_img_destroy(handle); + gd::elephc_img_destroy(decoded); + gd::elephc_img_destroy(decoded_tga); + let _ = std::fs::remove_dir_all(root); + } + + /// Oversized ABI buffer requests fail with null instead of overflowing a + /// length conversion or attempting an aborting allocation. + #[test] + fn oversized_transfer_buffers_fail_cleanly() { + assert!(codec::elephc_img_stage_ptr(i64::MAX).is_null()); + assert!(xfer::elephc_img_in_ptr(i64::MAX).is_null()); + } + + /// Bundled bitmap text renders a Latin-1 accented glyph without consulting + /// an OS font installation or a platform-specific font path. + #[test] + fn bundled_unicode_bitmap_font_renders_accented_glyph() { + let handle = gd::elephc_img_create_truecolor(8, 8); + let text = CString::new("é").expect("valid Unicode test text"); + unsafe { text::elephc_img_string(handle, 1, 0, 0, 0x00_ff_00_00, text.as_ptr()) }; + let guard = lock_recover(images()); + let rendered = guard + .get(&handle) + .is_some_and(|obj| obj.img.pixels().any(|pixel| pixel.0[0] != 0)); + drop(guard); + gd::elephc_img_destroy(handle); + assert!(rendered, "accented glyph was left blank"); + } +} diff --git a/crates/elephc-image/src/text.rs b/crates/elephc-image/src/text.rs index 5a0f325a7c..b95c178cca 100644 --- a/crates/elephc-image/src/text.rs +++ b/crates/elephc-image/src/text.rs @@ -14,6 +14,9 @@ //! - `font8x8` packs each glyph as eight row bytes with the least-significant bit //! as the leftmost column. Horizontal text advances 8 px per character; //! `imagestringup` rotates the layout 90° counter-clockwise. +//! - All bundled Unicode tables are consulted, so Latin accents, Greek, +//! Hiragana, box drawing, blocks, and miscellaneous symbols render without +//! filesystem fonts. Unsupported code points remain blank glyphs. //! - Rendering honors the image's alpha-blending mode via the shared //! `draw::plot` helper. @@ -25,7 +28,10 @@ use crate::{ffi_guard, lock_recover, cstr_arg, images, unpack_color}; /// set the layout is rotated 90° counter-clockwise (`imagestringup`). The `font` /// number is accepted for API parity but does not change the cell size. fn render_builtin(handle: i64, x: i64, y: i64, color: i64, text: &str, vertical: bool) { - use font8x8::{UnicodeFonts, BASIC_FONTS}; + use font8x8::{ + UnicodeFonts, BASIC_FONTS, BLOCK_FONTS, BOX_FONTS, GREEK_FONTS, HIRAGANA_FONTS, + LATIN_FONTS, MISC_FONTS, + }; let mut guard = lock_recover(images()); let Some(obj) = guard.get_mut(&handle) else { @@ -34,7 +40,15 @@ fn render_builtin(handle: i64, x: i64, y: i64, color: i64, text: &str, vertical: let blending = obj.alpha_blending; let src = unpack_color(color); for (index, ch) in text.chars().enumerate() { - let glyph = BASIC_FONTS.get(ch).unwrap_or([0u8; 8]); + let glyph = BASIC_FONTS + .get(ch) + .or_else(|| LATIN_FONTS.get(ch)) + .or_else(|| GREEK_FONTS.get(ch)) + .or_else(|| HIRAGANA_FONTS.get(ch)) + .or_else(|| BLOCK_FONTS.get(ch)) + .or_else(|| BOX_FONTS.get(ch)) + .or_else(|| MISC_FONTS.get(ch)) + .unwrap_or([0u8; 8]); let i = index as i64; for row in 0..8i64 { let bits = glyph[row as usize]; diff --git a/crates/elephc-image/src/xfer.rs b/crates/elephc-image/src/xfer.rs index 70108a1f3f..7d972adfe5 100644 --- a/crates/elephc-image/src/xfer.rs +++ b/crates/elephc-image/src/xfer.rs @@ -89,9 +89,15 @@ pub extern "C" fn elephc_img_in_ptr(len: i64) -> *mut u8 { if len <= 0 { return std::ptr::null_mut(); } + let Ok(len) = usize::try_from(len) else { + return std::ptr::null_mut(); + }; let mut guard = lock_recover(in_cell()); guard.clear(); - guard.resize(len as usize, 0); + if guard.try_reserve_exact(len).is_err() { + return std::ptr::null_mut(); + } + guard.resize(len, 0); guard.as_mut_ptr() }) } diff --git a/crates/elephc-magician/Cargo.toml b/crates/elephc-magician/Cargo.toml index 0090199bde..4bc49b66e6 100644 --- a/crates/elephc-magician/Cargo.toml +++ b/crates/elephc-magician/Cargo.toml @@ -13,6 +13,7 @@ crate-type = ["staticlib", "rlib"] elephc-crypto = { path = "../elephc-crypto" } elephc-phar = { path = "../elephc-phar" } flate2 = "1" +getrandom = "0.2" inventory = "0.3" libc = "0.2" unicode-segmentation = "1" diff --git a/crates/elephc-magician/build.rs b/crates/elephc-magician/build.rs index f50825d9fe..d45e827c21 100644 --- a/crates/elephc-magician/build.rs +++ b/crates/elephc-magician/build.rs @@ -9,23 +9,42 @@ //! binaries that link the rlib need the same native libraries as generated //! elephc binaries. -use std::{env, path::Path}; +use std::{ + env, + path::{Path, PathBuf}, +}; /// Emits native PCRE2 link directives for cargo-built test binaries and rlibs. fn main() { + println!("cargo:rerun-if-env-changed=ELEPHC_MINGW_SYSROOT"); for path in pcre2_library_search_paths() { - println!("cargo:rustc-link-search=native={path}"); + println!("cargo:rustc-link-search=native={}", path.display()); } println!("cargo:rustc-link-lib=pcre2-posix"); println!("cargo:rustc-link-lib=pcre2-8"); + if env::var("TARGET").as_deref() == Ok("x86_64-pc-windows-gnu") { + println!("cargo:rustc-link-lib=iconv"); + } if env::var("TARGET").as_deref() == Ok("aarch64-unknown-linux-musl") { println!("cargo:rustc-link-lib=gcc"); } } -/// Returns common PCRE2 library directories for local macOS/Homebrew builds. -fn pcre2_library_search_paths() -> Vec<&'static str> { - [ +/// Returns target-compatible PCRE2 library directories from the MinGW sysroot +/// and common local macOS/Homebrew installations. +fn pcre2_library_search_paths() -> Vec { + let mut paths = Vec::new(); + if env::var("TARGET").as_deref() == Ok("x86_64-pc-windows-gnu") { + if let Some(sysroot) = env::var_os("ELEPHC_MINGW_SYSROOT") { + let sysroot = PathBuf::from(sysroot); + for directory in [sysroot.join("lib"), sysroot.join("lib64")] { + if directory.is_dir() { + paths.push(directory); + } + } + } + } + paths.extend([ "/opt/homebrew/opt/pcre2/lib", "/opt/homebrew/lib", "/usr/local/opt/pcre2/lib", @@ -33,5 +52,6 @@ fn pcre2_library_search_paths() -> Vec<&'static str> { ] .into_iter() .filter(|path| Path::new(path).exists()) - .collect() + .map(PathBuf::from)); + paths } diff --git a/crates/elephc-magician/src/context/core.rs b/crates/elephc-magician/src/context/core.rs index e757874fb1..863eadb491 100644 --- a/crates/elephc-magician/src/context/core.rs +++ b/crates/elephc-magician/src/context/core.rs @@ -9,6 +9,25 @@ use super::*; +/// Stable lookup key for emulated POSIX permission bits on local files. +#[cfg(any(windows, test))] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub(crate) enum LocalFileModeKey { + /// Host filesystem identity, shared by every hard-link alias. + FileId { volume: u64, index: u64 }, + /// Canonical, platform-normalized path used when file identity is unavailable. + Path(String), +} + +/// Permission metadata captured before a filesystem mutation changes path reachability. +#[cfg(any(windows, test))] +#[derive(Clone, Debug)] +pub(crate) struct LocalFileModeToken { + pub(super) key: LocalFileModeKey, + pub(super) mode: u32, + pub(super) last_link: bool, +} + /// Process-level eval context passed opaquely across the C ABI. /// /// Generated code never inspects this layout directly; it only passes pointers @@ -74,6 +93,8 @@ pub struct ElephcEvalContext { pub(super) pending_throw: Option, pub(super) spl_autoload_extensions: String, pub(super) streams: EvalStreamResources, + #[cfg(any(windows, test))] + pub(super) local_file_modes: HashMap, pub(super) json_last_error: i64, pub(super) json_last_error_msg: String, pub(super) default_timezone: String, @@ -147,6 +168,8 @@ impl ElephcEvalContext { pending_throw: None, spl_autoload_extensions: String::from(".inc,.php"), streams: EvalStreamResources::default(), + #[cfg(any(windows, test))] + local_file_modes: HashMap::new(), json_last_error: 0, json_last_error_msg: String::from("No error"), default_timezone: String::from("UTC"), @@ -221,6 +244,8 @@ impl ElephcEvalContext { pending_throw: None, spl_autoload_extensions: String::from(".inc,.php"), streams: EvalStreamResources::default(), + #[cfg(any(windows, test))] + local_file_modes: HashMap::new(), json_last_error: 0, json_last_error_msg: String::from("No error"), default_timezone: String::from("UTC"), diff --git a/crates/elephc-magician/src/context/runtime_state.rs b/crates/elephc-magician/src/context/runtime_state.rs index 10f4d62111..85d1e884d8 100644 --- a/crates/elephc-magician/src/context/runtime_state.rs +++ b/crates/elephc-magician/src/context/runtime_state.rs @@ -111,6 +111,82 @@ impl ElephcEvalContext { self.included_files.insert(path.into()); } + /// Records PHP-visible permission bits against the file's stable identity. + #[cfg(any(windows, test))] + pub(crate) fn remember_local_file_mode( + &mut self, + path: impl AsRef, + mode: u32, + ) { + if let Some(key) = local_file_mode_key(path.as_ref()) { + self.local_file_modes.insert(key, mode & 0o7777); + } + } + + /// Returns emulated PHP permission bits for one local file, when present. + #[cfg(any(windows, test))] + pub(crate) fn local_file_mode(&self, path: impl AsRef) -> Option { + let key = local_file_mode_key(path.as_ref())?; + self.local_file_modes.get(&key).copied() + } + + /// Captures emulated mode state before rename/copy/link/unlink mutates paths. + #[cfg(any(windows, test))] + pub(crate) fn capture_local_file_mode( + &self, + path: impl AsRef, + ) -> Option { + let path = path.as_ref(); + let key = local_file_mode_key(path)?; + let mode = self.local_file_modes.get(&key).copied()?; + Some(LocalFileModeToken { + key, + mode, + last_link: local_file_link_count(path).is_none_or(|count| count <= 1), + }) + } + + /// Copies captured mode state onto a newly materialized destination file. + #[cfg(any(windows, test))] + pub(crate) fn copy_local_file_mode( + &mut self, + source: Option<&LocalFileModeToken>, + replaced_destination: Option<&LocalFileModeToken>, + destination: impl AsRef, + ) { + if let Some(replaced) = replaced_destination.filter(|token| token.last_link) { + self.local_file_modes.remove(&replaced.key); + } + if let Some(source) = source { + self.remember_local_file_mode(destination, source.mode); + } + } + + /// Moves captured mode state from the old path identity to the renamed file. + #[cfg(any(windows, test))] + pub(crate) fn rename_local_file_mode( + &mut self, + source: Option, + replaced_destination: Option, + destination: impl AsRef, + ) { + if let Some(replaced) = replaced_destination.filter(|token| token.last_link) { + self.local_file_modes.remove(&replaced.key); + } + if let Some(source) = source { + self.local_file_modes.remove(&source.key); + self.remember_local_file_mode(destination, source.mode); + } + } + + /// Purges captured mode state only when unlink removed the inode's last alias. + #[cfg(any(windows, test))] + pub(crate) fn unlink_local_file_mode(&mut self, removed: Option) { + if let Some(removed) = removed.filter(|token| token.last_link) { + self.local_file_modes.remove(&removed.key); + } + } + /// Stores the non-owned global scope handle used by eval `global` aliases. pub fn set_global_scope(&mut self, scope: *mut ElephcEvalScope) -> bool { if scope.is_null() { @@ -423,3 +499,238 @@ impl ElephcEvalContext { format!("{}({}) : eval()'d code", self.call_file, self.call_line) } } + +/// Builds an inode-like key, falling back to a normalized absolute path. +#[cfg(any(windows, test))] +fn local_file_mode_key(path: &std::path::Path) -> Option { + let metadata = std::fs::metadata(path).ok()?; + if let Some((volume, index)) = local_file_identity(path, &metadata) { + return Some(LocalFileModeKey::FileId { volume, index }); + } + Some(LocalFileModeKey::Path(normalize_local_file_mode_path(path)?)) +} + +/// Returns a stable host filesystem identity for hard-link-aware mode tracking. +#[cfg(any(windows, test))] +fn local_file_identity( + _path: &std::path::Path, + metadata: &std::fs::Metadata, +) -> Option<(u64, u64)> { + #[cfg(unix)] + { + use std::os::unix::fs::MetadataExt; + return Some((metadata.dev(), metadata.ino())); + } + #[cfg(windows)] + { + let _ = metadata; + let (volume, index, _) = windows_local_file_info(_path)?; + return Some((volume, index)); + } + #[cfg(not(any(unix, windows)))] + { + let _ = (_path, metadata); + None + } +} + +/// Returns the host hard-link count when the platform exposes it. +#[cfg(any(windows, test))] +fn local_file_link_count(path: &std::path::Path) -> Option { + let metadata = std::fs::metadata(path).ok()?; + #[cfg(unix)] + { + use std::os::unix::fs::MetadataExt; + return Some(metadata.nlink()); + } + #[cfg(windows)] + { + let _ = metadata; + return windows_local_file_info(path).map(|(_, _, links)| links); + } + #[cfg(not(any(unix, windows)))] + { + let _ = metadata; + None + } +} + +/// Canonicalizes an existing path and applies Windows case-insensitive folding. +#[cfg(any(windows, test))] +fn normalize_local_file_mode_path(path: &std::path::Path) -> Option { + let absolute = std::fs::canonicalize(path).or_else(|_| { + if path.is_absolute() { + Ok(path.to_path_buf()) + } else { + std::env::current_dir().map(|cwd| cwd.join(path)) + } + }); + let normalized = absolute.ok()?.to_string_lossy().into_owned(); + #[cfg(windows)] + return Some(normalized.to_lowercase()); + #[cfg(not(windows))] + return Some(normalized); +} + +/// Reads stable Windows volume, file-index, and hard-link-count fields. +#[cfg(windows)] +fn windows_local_file_info(path: &std::path::Path) -> Option<(u64, u64, u64)> { + use std::ffi::c_void; + use std::os::windows::io::AsRawHandle; + + #[repr(C)] + struct FileTime { + low: u32, + high: u32, + } + + #[repr(C)] + struct ByHandleFileInformation { + attributes: u32, + creation_time: FileTime, + last_access_time: FileTime, + last_write_time: FileTime, + volume_serial: u32, + file_size_high: u32, + file_size_low: u32, + number_of_links: u32, + file_index_high: u32, + file_index_low: u32, + } + + #[link(name = "kernel32")] + unsafe extern "system" { + /// Reads stable filesystem identity for one open Windows handle. + fn GetFileInformationByHandle( + file: *mut c_void, + information: *mut ByHandleFileInformation, + ) -> i32; + } + + let file = std::fs::File::open(path).ok()?; + let mut information = std::mem::MaybeUninit::::uninit(); + let status = unsafe { + GetFileInformationByHandle(file.as_raw_handle(), information.as_mut_ptr()) + }; + if status == 0 { + return None; + } + let information = unsafe { information.assume_init() }; + Some(( + u64::from(information.volume_serial), + (u64::from(information.file_index_high) << 32) + | u64::from(information.file_index_low), + u64::from(information.number_of_links), + )) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + use std::sync::atomic::{AtomicU64, Ordering}; + + static NEXT_FIXTURE_ID: AtomicU64 = AtomicU64::new(0); + + /// Creates an isolated filesystem fixture directory for one mode-state test. + fn mode_test_dir(label: &str) -> PathBuf { + let id = NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed); + let path = std::env::temp_dir().join(format!( + "elephc_magician_mode_{label}_{}_{id}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&path); + std::fs::create_dir(&path).expect("create mode test directory"); + path + } + + /// Verifies canonical and dot-segment aliases resolve to the same mode identity. + #[test] + fn local_file_mode_resolves_path_aliases() { + let dir = mode_test_dir("aliases"); + let file = dir.join("sample.txt"); + std::fs::write(&file, b"data").expect("create mode test file"); + let alias = dir.join(".").join("sample.txt"); + let canonical = std::fs::canonicalize(&file).expect("canonicalize mode test file"); + let mut context = ElephcEvalContext::new(); + + context.remember_local_file_mode(&alias, 0o640); + + assert_eq!(context.local_file_mode(&file), Some(0o640)); + assert_eq!(context.local_file_mode(&canonical), Some(0o640)); + std::fs::remove_dir_all(dir).expect("remove mode test directory"); + } + + /// Verifies rename moves mode state and copy replaces destination mode state. + #[test] + fn local_file_mode_follows_rename_and_copy() { + let dir = mode_test_dir("rename_copy"); + let source = dir.join("source.txt"); + let renamed = dir.join("renamed.txt"); + let copied = dir.join("copied.txt"); + std::fs::write(&source, b"source").expect("create source file"); + std::fs::write(&copied, b"old destination").expect("create copy destination"); + let mut context = ElephcEvalContext::new(); + context.remember_local_file_mode(&source, 0o600); + context.remember_local_file_mode(&copied, 0o777); + + let source_mode = context.capture_local_file_mode(&source); + let renamed_mode = context.capture_local_file_mode(&renamed); + std::fs::rename(&source, &renamed).expect("rename source file"); + context.rename_local_file_mode(source_mode, renamed_mode, &renamed); + assert_eq!(context.local_file_mode(&source), None); + assert_eq!(context.local_file_mode(&renamed), Some(0o600)); + + let source_mode = context.capture_local_file_mode(&renamed); + let destination_mode = context.capture_local_file_mode(&copied); + std::fs::copy(&renamed, &copied).expect("copy renamed file"); + context.copy_local_file_mode(source_mode.as_ref(), destination_mode.as_ref(), &copied); + assert_eq!(context.local_file_mode(&renamed), Some(0o600)); + assert_eq!(context.local_file_mode(&copied), Some(0o600)); + std::fs::remove_dir_all(dir).expect("remove mode test directory"); + } + + /// Verifies hard-link aliases retain mode state until the last link is removed. + #[test] + fn local_file_mode_tracks_hard_links_and_recreation() { + let dir = mode_test_dir("hard_link"); + let source = dir.join("source.txt"); + let alias = dir.join("alias.txt"); + std::fs::write(&source, b"source").expect("create hard-link source"); + let mut context = ElephcEvalContext::new(); + context.remember_local_file_mode(&source, 0o620); + + let source_mode = context.capture_local_file_mode(&source); + std::fs::hard_link(&source, &alias).expect("create hard-link alias"); + context.copy_local_file_mode(source_mode.as_ref(), None, &alias); + assert_eq!(context.local_file_mode(&alias), Some(0o620)); + + let removed_source = context.capture_local_file_mode(&source); + std::fs::remove_file(&source).expect("remove original hard link"); + context.unlink_local_file_mode(removed_source); + assert_eq!(context.local_file_mode(&alias), Some(0o620)); + + let removed_alias = context.capture_local_file_mode(&alias); + std::fs::remove_file(&alias).expect("remove final hard link"); + context.unlink_local_file_mode(removed_alias); + std::fs::write(&alias, b"replacement").expect("recreate alias path"); + assert_eq!(context.local_file_mode(&alias), None); + std::fs::remove_dir_all(dir).expect("remove mode test directory"); + } + + /// Verifies Windows case-insensitive path aliases share one emulated mode. + #[cfg(windows)] + #[test] + fn local_file_mode_resolves_windows_case_aliases() { + let dir = mode_test_dir("case_alias"); + let file = dir.join("CaseSample.txt"); + std::fs::write(&file, b"data").expect("create Windows case test file"); + let case_alias = dir.join("casesample.TXT"); + let mut context = ElephcEvalContext::new(); + + context.remember_local_file_mode(&file, 0o604); + + assert_eq!(context.local_file_mode(&case_alias), Some(0o604)); + std::fs::remove_dir_all(dir).expect("remove mode test directory"); + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/basename.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/basename.rs index 5613408937..2b5d346477 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/basename.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/basename.rs @@ -80,14 +80,14 @@ pub(in crate::interpreter) fn eval_basename_result( /// Extracts a PHP basename from one path byte string. pub(in crate::interpreter) fn eval_basename_bytes(path: &[u8], suffix: Option<&[u8]>) -> Vec { let mut end = path.len(); - while end > 0 && path[end - 1] == b'/' { + while end > 0 && eval_basename_separator(path[end - 1]) { end -= 1; } if end == 0 { return Vec::new(); } let mut start = end; - while start > 0 && path[start - 1] != b'/' { + while start > 0 && !eval_basename_separator(path[start - 1]) { start -= 1; } let mut result = path[start..end].to_vec(); @@ -98,3 +98,8 @@ pub(in crate::interpreter) fn eval_basename_bytes(path: &[u8], suffix: Option<&[ } result } + +/// Returns whether one byte separates path components on the current host platform. +fn eval_basename_separator(byte: u8) -> bool { + byte == b'/' || cfg!(windows) && byte == b'\\' +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/chmod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/chmod.rs index 0f580a1f8f..7fd4c3e62e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/chmod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/chmod.rs @@ -78,6 +78,21 @@ pub(in crate::interpreter) fn eval_chmod_result( let Some(path) = stream_wrappers::local_filesystem_path(&path) else { return values.bool_value(false); }; + #[cfg(unix)] let permissions = std::fs::Permissions::from_mode(mode); - values.bool_value(std::fs::set_permissions(path, permissions).is_ok()) + #[cfg(windows)] + let permissions = { + let Ok(metadata) = std::fs::metadata(&path) else { + return values.bool_value(false); + }; + let mut permissions = metadata.permissions(); + permissions.set_readonly(mode & 0o222 == 0); + permissions + }; + let changed = std::fs::set_permissions(&path, permissions).is_ok(); + #[cfg(windows)] + if changed { + context.remember_local_file_mode(path, mode); + } + values.bool_value(changed) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/chown.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/chown.rs index ad33ac4f01..b2e51bcdc2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/chown.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/chown.rs @@ -16,6 +16,7 @@ eval_builtin! { } use super::super::super::*; +#[cfg(unix)] use std::ffi::CString; use crate::stream_wrappers; use super::*; @@ -79,6 +80,17 @@ pub(in crate::interpreter) fn eval_chown_like_result( let Some(path) = stream_wrappers::local_filesystem_path(&path) else { return values.bool_value(false); }; + eval_local_chown(name, &path, principal, values) +} + +/// Applies Unix owner/group changes to a local filesystem path. +#[cfg(unix)] +fn eval_local_chown( + name: &str, + path: &str, + principal: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { let Some(path) = eval_c_string(&path) else { return values.bool_value(false); }; @@ -95,6 +107,23 @@ pub(in crate::interpreter) fn eval_chown_like_result( values.bool_value(status == 0) } +/// Reports unsupported Windows owner/group changes without claiming success. +#[cfg(windows)] +fn eval_local_chown( + name: &str, + _path: &str, + principal: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match (name, values.type_tag(principal)?) { + ("chown" | "chgrp" | "lchown" | "lchgrp", EVAL_TAG_INT | EVAL_TAG_STRING) => { + values.bool_value(false) + } + ("chown" | "chgrp" | "lchown" | "lchgrp", _) => Err(EvalStatus::RuntimeFatal), + _ => Err(EvalStatus::RuntimeFatal), + } +} + /// Builds the wrapper metadata option and value for `chown()` or `chgrp()`. fn eval_chown_metadata_arg( name: &str, @@ -118,6 +147,7 @@ fn eval_chown_metadata_arg( } /// Resolves one PHP owner/group argument into libc uid/gid slots. +#[cfg(unix)] fn eval_chown_principal_ids( name: &str, principal: RuntimeCellHandle, @@ -148,6 +178,7 @@ fn eval_chown_principal_ids( } /// Resolves a PHP user-name cell to a libc uid. +#[cfg(unix)] fn eval_owner_name_id( principal: RuntimeCellHandle, values: &mut impl RuntimeValueOps, @@ -165,6 +196,7 @@ fn eval_owner_name_id( } /// Resolves a PHP group-name cell to a libc gid. +#[cfg(unix)] fn eval_group_name_id( principal: RuntimeCellHandle, values: &mut impl RuntimeValueOps, @@ -182,11 +214,13 @@ fn eval_group_name_id( } /// Converts a Rust path string into a C string, rejecting embedded NUL bytes. +#[cfg(unix)] fn eval_c_string(value: &str) -> Option { CString::new(value).ok() } /// Converts raw PHP bytes into a C string, rejecting embedded NUL bytes. +#[cfg(unix)] fn eval_c_bytes(value: &[u8]) -> Option { CString::new(value).ok() } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/copy.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/copy.rs index 2e762e9257..7b71770e87 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/copy.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/copy.rs @@ -78,12 +78,45 @@ pub(in crate::interpreter) fn eval_binary_path_bool_result( let Some(to) = stream_wrappers::local_filesystem_path(&to) else { return values.bool_value(false); }; + #[cfg(windows)] + let source_mode = context.capture_local_file_mode(&from); + #[cfg(windows)] + let destination_mode = context.capture_local_file_mode(&to); let ok = match name { - "copy" => std::fs::copy(from, to).is_ok(), - "link" => std::fs::hard_link(from, to).is_ok(), - "rename" => std::fs::rename(from, to).is_ok(), - "symlink" => std::os::unix::fs::symlink(from, to).is_ok(), + "copy" => std::fs::copy(&from, &to).is_ok(), + "link" => std::fs::hard_link(&from, &to).is_ok(), + "rename" => std::fs::rename(&from, &to).is_ok(), + "symlink" => eval_create_symlink(&from, &to), _ => return Err(EvalStatus::RuntimeFatal), }; + #[cfg(windows)] + if ok { + match name { + "copy" | "link" => context.copy_local_file_mode( + source_mode.as_ref(), + destination_mode.as_ref(), + &to, + ), + "rename" => context.rename_local_file_mode(source_mode, destination_mode, &to), + "symlink" => {} + _ => unreachable!("filesystem operation was validated above"), + } + } values.bool_value(ok) } + +/// Creates a symbolic link with the host-specific path API. +fn eval_create_symlink(from: &str, to: &str) -> bool { + #[cfg(unix)] + { + std::os::unix::fs::symlink(from, to).is_ok() + } + #[cfg(windows)] + { + if std::fs::metadata(from).is_ok_and(|metadata| metadata.is_dir()) { + std::os::windows::fs::symlink_dir(from, to).is_ok() + } else { + std::os::windows::fs::symlink_file(from, to).is_ok() + } + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs index ce4dbb5154..fcd91397c7 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs @@ -85,6 +85,10 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call( "pclose" => super::pclose::eval_pclose_declared_call(args, context, scope, values), "pfsockopen" => super::pfsockopen::eval_pfsockopen_declared_call(args, context, scope, values), "popen" => super::popen::eval_popen_declared_call(args, context, scope, values), + "proc_close" => super::proc_close::eval_proc_close_declared_call(args, context, scope, values), + "proc_get_status" => super::proc_get_status::eval_proc_get_status_declared_call(args, context, scope, values), + "proc_open" => super::proc_open::eval_proc_open_declared_call(args, context, scope, values), + "proc_terminate" => super::proc_terminate::eval_proc_terminate_declared_call(args, context, scope, values), "readdir" => super::readdir::eval_readdir_declared_call(args, context, scope, values), "readfile" => super::readfile::eval_readfile_declared_call(args, context, scope, values), "readline" => super::readline::eval_readline_declared_call(args, context, scope, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/dirname.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/dirname.rs index 0a51247a95..753cf16edf 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/dirname.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/dirname.rs @@ -90,7 +90,7 @@ pub(in crate::interpreter) fn eval_dirname_once(path: &[u8]) -> Vec { return b".".to_vec(); } let mut end = path.len(); - while end > 0 && path[end - 1] == b'/' { + while end > 0 && eval_dirname_separator(path[end - 1]) { end -= 1; } if end == 0 { @@ -99,9 +99,9 @@ pub(in crate::interpreter) fn eval_dirname_once(path: &[u8]) -> Vec { let mut cursor = end; while cursor > 0 { cursor -= 1; - if path[cursor] == b'/' { + if eval_dirname_separator(path[cursor]) { let mut parent_end = cursor; - while parent_end > 0 && path[parent_end - 1] == b'/' { + while parent_end > 0 && eval_dirname_separator(path[parent_end - 1]) { parent_end -= 1; } return if parent_end == 0 { @@ -113,3 +113,8 @@ pub(in crate::interpreter) fn eval_dirname_once(path: &[u8]) -> Vec { } b".".to_vec() } + +/// Returns whether one byte separates path components on the current host platform. +fn eval_dirname_separator(byte: u8) -> bool { + byte == b'/' || cfg!(windows) && byte == b'\\' +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_free_space.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_free_space.rs index 206167594f..3bf460be94 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_free_space.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_free_space.rs @@ -61,8 +61,18 @@ pub(in crate::interpreter) fn eval_disk_space_result( values: &mut impl RuntimeValueOps, ) -> Result { let bytes = values.string_bytes(directory)?; + #[cfg(unix)] + let result = eval_disk_space_bytes_unix(name, &bytes)?; + #[cfg(windows)] + let result = eval_disk_space_bytes_windows(name, &bytes)?; + values.float(result) +} + +/// Queries Unix filesystem capacity through `statvfs`. +#[cfg(unix)] +fn eval_disk_space_bytes_unix(name: &str, bytes: &[u8]) -> Result { let Ok(path) = CString::new(bytes) else { - return values.float(0.0); + return Ok(0.0); }; let mut stats = std::mem::MaybeUninit::::zeroed(); let status = unsafe { @@ -70,7 +80,7 @@ pub(in crate::interpreter) fn eval_disk_space_result( libc::statvfs(path.as_ptr(), stats.as_mut_ptr()) }; if status != 0 { - return values.float(0.0); + return Ok(0.0); } let stats = unsafe { // `statvfs` succeeded, so libc initialized the full stat buffer. @@ -86,5 +96,48 @@ pub(in crate::interpreter) fn eval_disk_space_result( "disk_total_space" => stats.f_blocks, _ => return Err(EvalStatus::RuntimeFatal), }; - values.float((block_size as f64) * (blocks as f64)) + Ok((block_size as f64) * (blocks as f64)) +} + +/// Queries Windows filesystem capacity through `GetDiskFreeSpaceExW`. +#[cfg(windows)] +fn eval_disk_space_bytes_windows(name: &str, bytes: &[u8]) -> Result { + use std::os::windows::ffi::OsStrExt; + + #[link(name = "kernel32")] + unsafe extern "system" { + /// Reads total and available byte counts for the filesystem containing a Windows path. + fn GetDiskFreeSpaceExW( + directory: *const u16, + free_for_caller: *mut u64, + total_bytes: *mut u64, + total_free: *mut u64, + ) -> i32; + } + + let path = String::from_utf8_lossy(bytes); + let wide: Vec = std::ffi::OsStr::new(path.as_ref()) + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + let mut available = 0_u64; + let mut total = 0_u64; + let mut free = 0_u64; + let status = unsafe { + GetDiskFreeSpaceExW( + wide.as_ptr(), + &mut available, + &mut total, + &mut free, + ) + }; + if status == 0 { + return Ok(0.0); + } + let bytes = match name { + "disk_free_space" => available, + "disk_total_space" => total, + _ => return Err(EvalStatus::RuntimeFatal), + }; + Ok(bytes as f64) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/filetype.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/filetype.rs index 711016f787..11ba0a448c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/filetype.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/filetype.rs @@ -88,7 +88,16 @@ pub(in crate::interpreter) fn eval_filetype_result( "dir" } else if file_type.is_symlink() { "link" - } else if file_type.is_char_device() { + } else { + eval_special_filetype(&file_type) + }; + values.string(label) +} + +/// Classifies Unix special file kinds that have no Windows filesystem equivalent. +#[cfg(unix)] +fn eval_special_filetype(file_type: &std::fs::FileType) -> &'static str { + if file_type.is_char_device() { "char" } else if file_type.is_block_device() { "block" @@ -98,6 +107,11 @@ pub(in crate::interpreter) fn eval_filetype_result( "socket" } else { "unknown" - }; - values.string(label) + } +} + +/// Classifies Windows special files as unknown outside files, directories, and links. +#[cfg(windows)] +fn eval_special_filetype(_file_type: &std::fs::FileType) -> &'static str { + "unknown" } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/lchgrp.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/lchgrp.rs index 380b0309f4..64e8288632 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/lchgrp.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/lchgrp.rs @@ -7,6 +7,7 @@ //! Key details: //! - Runtime dispatch is declared here and delegated through the ownership/group helper. +#[cfg(not(windows))] eval_builtin! { name: "lchgrp", area: Filesystem, diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/lchown.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/lchown.rs index 11df55d24e..bb02320d29 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/lchown.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/lchown.rs @@ -7,6 +7,7 @@ //! Key details: //! - Runtime dispatch is declared here and delegated through the ownership/group helper. +#[cfg(not(windows))] eval_builtin! { name: "lchown", area: Filesystem, diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/linkinfo.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/linkinfo.rs index 1a5b477830..6e5cb8e4ae 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/linkinfo.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/linkinfo.rs @@ -63,9 +63,12 @@ pub(in crate::interpreter) fn eval_linkinfo_result( let Some(path) = stream_wrappers::local_filesystem_path(&path) else { return values.int(-1); }; + #[cfg(unix)] let dev = match std::fs::symlink_metadata(path) { Ok(metadata) => i64::try_from(metadata.dev()).map_err(|_| EvalStatus::RuntimeFatal)?, Err(_) => -1, }; + #[cfg(windows)] + let dev = if std::fs::symlink_metadata(path).is_ok() { 0 } else { -1 }; values.int(dev) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs index 774fad9725..378c9748ad 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs @@ -77,6 +77,10 @@ mod pathinfo; mod pclose; mod pfsockopen; mod popen; +mod proc_close; +mod proc_get_status; +mod proc_open; +mod proc_terminate; mod readdir; mod readfile; mod readline; @@ -131,7 +135,7 @@ mod stream_wrapper_restore; mod stream_wrapper_unregister; mod streams; mod symlink; -mod sys_get_temp_dir; +pub(in crate::interpreter) mod sys_get_temp_dir; mod tempnam; mod tmpfile; mod touch; @@ -168,6 +172,7 @@ pub(in crate::interpreter) use flock::{eval_builtin_flock, eval_flock_result}; pub(in crate::interpreter) use fsockopen::{ eval_builtin_fsockopen_call, eval_fsockopen_with_error_result, }; +pub(in crate::interpreter) use proc_open::eval_builtin_proc_open_call; pub(in crate::interpreter) use stream_select::{ eval_builtin_stream_select_call, eval_stream_select_result, }; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/path.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/path.rs index b313415c73..9dd83e7beb 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/path.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/path.rs @@ -25,9 +25,22 @@ pub(in crate::interpreter) fn eval_path_is_readable(path: &std::path::Path) -> b /// Returns whether a path has any executable bit set in its Unix mode. pub(in crate::interpreter) fn eval_path_is_executable(path: &std::path::Path) -> bool { + #[cfg(unix)] + { std::fs::metadata(path) .map(|metadata| metadata.mode() & 0o111 != 0) .unwrap_or(false) + } + #[cfg(windows)] + { + path.is_file() + && path + .extension() + .and_then(std::ffi::OsStr::to_str) + .is_some_and(|extension| { + matches!(extension.to_ascii_lowercase().as_str(), "exe" | "com" | "bat" | "cmd") + }) + } } /// Returns whether a path can be written by the current process. diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/proc_close.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/proc_close.rs new file mode 100644 index 0000000000..2a43a652cd --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/proc_close.rs @@ -0,0 +1,62 @@ +//! Purpose: +//! Declarative eval registry entry and process-resource implementation for `proc_close`. +//! +//! Called from: +//! - Eval builtin registry filesystem dispatch. +//! +//! Key details: +//! - Only process resources created by eval `proc_open` are accepted and waited. + +eval_builtin! { + name: "proc_close", + area: Filesystem, + params: [process], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates a direct `proc_close(process)` call. +pub(in crate::interpreter) fn eval_proc_close_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [process] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let process = eval_expr(process, context, scope, values)?; + eval_proc_close_result(process, context, values) +} + +/// Evaluates `proc_close` from normalized argument values. +pub(in crate::interpreter) fn eval_proc_close_declared_values_result( + args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [process] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_proc_close_result(*process, context, values) +} + +/// Waits for an eval process resource and returns its child exit status. +fn eval_proc_close_result( + process: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(process)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let id = eval_int_value(process, values)? + .checked_sub(1) + .ok_or(EvalStatus::RuntimeFatal)?; + match context.stream_resources_mut().close_process(id) { + Some(status) => values.int(status), + None => values.bool_value(false), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/proc_get_status.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/proc_get_status.rs new file mode 100644 index 0000000000..1b932b9910 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/proc_get_status.rs @@ -0,0 +1,78 @@ +//! Purpose: +//! Declares and implements eval-time `proc_get_status` for `proc_open` resources. +//! +//! Called from: +//! - The filesystem builtin direct and normalized-value dispatchers. +//! +//! Key details: +//! - Status inspection is non-consuming so a later `proc_close` can still wait. + +eval_builtin! { + name: "proc_get_status", + area: Filesystem, + params: [process], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates a direct `proc_get_status(process)` call. +pub(in crate::interpreter) fn eval_proc_get_status_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [process] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let process = eval_expr(process, context, scope, values)?; + eval_proc_get_status_result(process, context, values) +} + +/// Evaluates `proc_get_status` from normalized argument values. +pub(in crate::interpreter) fn eval_proc_get_status_declared_values_result( + args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [process] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_proc_get_status_result(*process, context, values) +} + +/// Builds PHP's process-status array for one live eval process resource. +fn eval_proc_get_status_result( + process: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(process)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let id = eval_int_value(process, values)? + .checked_sub(1) + .ok_or(EvalStatus::RuntimeFatal)?; + let Some(status) = context.stream_resources_mut().process_status(id) else { + return values.bool_value(false); + }; + let entries = [ + ("command", values.string(&status.command)?), + ("pid", values.int(status.pid)?), + ("cached", values.bool_value(status.cached)?), + ("running", values.bool_value(status.running)?), + ("signaled", values.bool_value(status.signaled)?), + ("stopped", values.bool_value(status.stopped)?), + ("exitcode", values.int(status.exitcode)?), + ("termsig", values.int(status.termsig)?), + ("stopsig", values.int(status.stopsig)?), + ]; + let mut result = values.array_new(entries.len())?; + for (name, value) in entries { + let name = values.string(name)?; + result = values.array_set(result, name, value)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/proc_open.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/proc_open.rs new file mode 100644 index 0000000000..caa961cb3e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/proc_open.rs @@ -0,0 +1,220 @@ +//! Purpose: +//! Declarative eval registry entry and process-resource implementation for `proc_open`. +//! +//! Called from: +//! - Eval builtin registry dispatch and the source-sensitive call dispatcher. +//! +//! Key details: +//! - The pipes argument is written by reference and process ownership remains in +//! `EvalStreamResources` until `proc_close` waits for it. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "proc_open", + area: Filesystem, + params: [ + command, + descriptor_spec, + pipes: by_ref, + cwd = EvalBuiltinDefaultValue::Null, + env_vars = EvalBuiltinDefaultValue::Null, + options = EvalBuiltinDefaultValue::Null + ], + by_ref: [pipes], + direct: none, + values: Filesystem, +} + +use super::super::super::*; +use super::*; +use crate::stream_resources::EvalProcDescriptor; + +/// Evaluates positional `proc_open` calls that cannot preserve a writable pipes target. +pub(in crate::interpreter) fn eval_proc_open_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(3..=6).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let mut evaluated = Vec::with_capacity(args.len()); + for arg in args { + evaluated.push(eval_expr(arg, context, scope, values)?); + } + values.warning("proc_open(): Argument #3 ($pipes) must be passed by reference, value given")?; + eval_proc_open_values(&evaluated, None, context, values) +} + +/// Evaluates materialized `proc_open` arguments without a caller lvalue. +pub(in crate::interpreter) fn eval_proc_open_declared_values_result( + args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_proc_open_values(args, None, context, values) +} + +/// Evaluates source call metadata while retaining the by-reference pipes lvalue. +pub(in crate::interpreter) fn eval_builtin_proc_open_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated = eval_call_arg_values(args, context, scope, values)?; + let (bound, _) = bind_evaluated_ref_builtin_args( + &["command", "descriptor_spec", "pipes", "cwd", "env_vars", "options"], + &evaluated, + false, + )?; + let command = required_evaluated_ref_arg(&bound, 0)?; + let descriptor = required_evaluated_ref_arg(&bound, 1)?; + let pipes = required_evaluated_ref_arg(&bound, 2)?; + let target = pipes.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)?; + let mut selected = vec![command.value, descriptor.value, pipes.value]; + for index in 3..=5 { + if let Some(arg) = optional_evaluated_ref_arg(&bound, index) { + selected.push(arg.value); + } + } + eval_proc_open_values(&selected, Some(&target), context, values) +} + +/// Starts one shell process and returns its eval-local process resource. +fn eval_proc_open_values( + args: &[RuntimeCellHandle], + pipes_target: Option<&EvalReferenceTarget>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(3..=6).contains(&args.len()) || !values.is_array_like(args[1])? { + return Err(EvalStatus::RuntimeFatal); + } + let command = eval_path_string(args[0], values)?; + let descriptors = eval_proc_descriptors(args[1], values)?; + let cwd = match args.get(3).copied() { + Some(value) if values.type_tag(value)? != EVAL_TAG_NULL => { + Some(eval_path_string(value, values)?) + } + _ => None, + }; + let env = match args.get(4).copied() { + Some(value) if values.type_tag(value)? != EVAL_TAG_NULL => { + Some(eval_proc_environment(value, values)?) + } + _ => None, + }; + let bypass_shell = match args.get(5).copied() { + Some(value) if values.type_tag(value)? != EVAL_TAG_NULL => { + let key = values.string("bypass_shell")?; + let option = values.array_get(value, key)?; + values.type_tag(option)? != EVAL_TAG_NULL && values.truthy(option)? + } + _ => false, + }; + match context.stream_resources_mut().open_process( + &command, + &descriptors, + cwd.as_deref(), + env.as_deref(), + bypass_shell, + ) { + Some(result) => { + let mut pipes = values.array_new(result.pipes.len())?; + for (descriptor, pipe_id) in result.pipes { + let key = values.int(descriptor)?; + let pipe = values.resource(pipe_id)?; + pipes = values.array_set(pipes, key, pipe)?; + } + if let Some(target) = pipes_target { + eval_write_direct_ref_target( + target, + pipes, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + values.resource(result.process_id) + } + None => values.bool_value(false), + } +} + +/// Parses PHP's descriptor specification into the three child stdio slots. +fn eval_proc_descriptors( + spec: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<[Option; 3], EvalStatus> { + let mut descriptors = std::array::from_fn(|_| None); + for position in 0..values.array_len(spec)? { + let key = values.array_iter_key(spec, position)?; + let descriptor = usize::try_from(eval_int_value(key, values)?) + .ok() + .filter(|descriptor| *descriptor < descriptors.len()) + .ok_or(EvalStatus::RuntimeFatal)?; + let entry = values.array_get(spec, key)?; + if values.type_tag(entry)? == EVAL_TAG_INT { + let target = usize::try_from(eval_int_value(entry, values)?) + .ok() + .filter(|target| *target < descriptors.len()) + .ok_or(EvalStatus::RuntimeFatal)?; + descriptors[descriptor] = Some(EvalProcDescriptor::Redirect(target)); + continue; + } + if !values.is_array_like(entry)? { + return Err(EvalStatus::RuntimeFatal); + } + let kind = eval_proc_descriptor_string(entry, 0, values)?; + descriptors[descriptor] = Some(match kind.as_str() { + "pipe" => { + let mode = eval_proc_descriptor_string(entry, 1, values)?; + match mode.as_str() { + "r" => EvalProcDescriptor::Pipe { child_reads: true }, + "w" => EvalProcDescriptor::Pipe { child_reads: false }, + _ => return Err(EvalStatus::RuntimeFatal), + } + } + "file" => EvalProcDescriptor::File { + path: eval_proc_descriptor_string(entry, 1, values)?, + mode: eval_proc_descriptor_string(entry, 2, values)?, + }, + _ => return Err(EvalStatus::RuntimeFatal), + }); + } + Ok(descriptors) +} + +/// Reads one string field from an indexed descriptor tuple. +fn eval_proc_descriptor_string( + descriptor: RuntimeCellHandle, + index: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.int(index)?; + let value = values.array_get(descriptor, key)?; + if values.type_tag(value)? != EVAL_TAG_STRING { + return Err(EvalStatus::RuntimeFatal); + } + eval_path_string(value, values) +} + +/// Converts a PHP environment array into the exact child environment mapping. +fn eval_proc_environment( + env: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !values.is_array_like(env)? { + return Err(EvalStatus::RuntimeFatal); + } + let mut result = Vec::with_capacity(values.array_len(env)?); + for position in 0..values.array_len(env)? { + let key = values.array_iter_key(env, position)?; + let value = values.array_get(env, key)?; + result.push((eval_path_string(key, values)?, eval_path_string(value, values)?)); + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/proc_terminate.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/proc_terminate.rs new file mode 100644 index 0000000000..c8b5a943d9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/proc_terminate.rs @@ -0,0 +1,73 @@ +//! Purpose: +//! Declares and implements eval-time process termination for `proc_open` resources. +//! +//! Called from: +//! - The filesystem builtin direct and normalized-value dispatchers. +//! +//! Key details: +//! - Unix forwards the requested signal while Windows uses the child termination API. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "proc_terminate", + area: Filesystem, + params: [process, signal = EvalBuiltinDefaultValue::Int(15)], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates a direct `proc_terminate(process, signal = 15)` call. +pub(in crate::interpreter) fn eval_proc_terminate_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (process, signal) = match args { + [process] => (eval_expr(process, context, scope, values)?, 15), + [process, signal] => ( + eval_expr(process, context, scope, values)?, + eval_int_value(eval_expr(signal, context, scope, values)?, values)?, + ), + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_proc_terminate_result(process, signal, context, values) +} + +/// Evaluates `proc_terminate` from normalized argument values. +pub(in crate::interpreter) fn eval_proc_terminate_declared_values_result( + args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (process, signal) = match args { + [process] => (*process, 15), + [process, signal] => (*process, eval_int_value(*signal, values)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_proc_terminate_result(process, signal, context, values) +} + +/// Terminates one eval process resource and returns PHP's boolean success result. +fn eval_proc_terminate_result( + process: RuntimeCellHandle, + signal: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(process)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let id = eval_int_value(process, values)? + .checked_sub(1) + .ok_or(EvalStatus::RuntimeFatal)?; + values.bool_value( + context + .stream_resources_mut() + .terminate_process(id, signal) + .unwrap_or(false), + ) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stat.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stat.rs index 675053d8ba..37479733c2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stat.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stat.rs @@ -18,6 +18,8 @@ eval_builtin! { use super::super::super::*; use crate::stream_wrappers; use super::*; +#[cfg(windows)] +use std::os::windows::fs::MetadataExt as WindowsMetadataExt; /// Dispatches direct eval calls for the `stat` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_stat_declared_call( @@ -73,11 +75,12 @@ pub(in crate::interpreter) fn eval_file_stat_scalar_result( _ => values.bool_value(false), }; }; - let metadata = match std::fs::metadata(path) { + let metadata = match std::fs::metadata(&path) { Ok(metadata) => metadata, Err(_) if name == "filemtime" => return values.int(0), Err(_) => return values.bool_value(false), }; + #[cfg(unix)] match name { "fileatime" => values.int(metadata.atime()), "filectime" => values.int(metadata.ctime()), @@ -90,6 +93,29 @@ pub(in crate::interpreter) fn eval_file_stat_scalar_result( "fileperms" => values.int(i64::from(metadata.mode())), _ => Err(EvalStatus::RuntimeFatal), } + #[cfg(windows)] + let windows_info = eval_windows_file_info(&path); + #[cfg(windows)] + match name { + "fileatime" => values.int(eval_windows_filetime_seconds(metadata.last_access_time())), + "filectime" => values.int(eval_windows_filetime_seconds(metadata.creation_time())), + "filegroup" | "fileowner" => values.int(0), + "fileinode" => values.int( + windows_info + .and_then(|info| i64::try_from(info.file_index).ok()) + .unwrap_or(0), + ), + "filemtime" => values.int(eval_windows_filetime_seconds(metadata.last_write_time())), + "fileperms" => { + let physical_mode = eval_windows_metadata_mode(&metadata); + let mode = context + .local_file_mode(&path) + .map(|permissions| (physical_mode & !0o7777) | i64::from(permissions)) + .unwrap_or(physical_mode); + values.int(mode) + } + _ => Err(EvalStatus::RuntimeFatal), + } } /// Evaluates PHP `stat($filename)` or `lstat($filename)` over one eval expression. pub(in crate::interpreter) fn eval_builtin_stat_array( @@ -121,15 +147,23 @@ pub(in crate::interpreter) fn eval_stat_array_result( return values.bool_value(false); }; let metadata = match name { - "stat" => std::fs::metadata(path), - "lstat" => std::fs::symlink_metadata(path), + "stat" => std::fs::metadata(&path), + "lstat" => std::fs::symlink_metadata(&path), _ => return Err(EvalStatus::RuntimeFatal), }; let metadata = match metadata { Ok(metadata) => metadata, Err(_) => return values.bool_value(false), }; - eval_stat_metadata_array(&metadata, values) + #[cfg(unix)] + return eval_stat_metadata_array(&metadata, values); + #[cfg(windows)] + eval_stat_metadata_array_with_windows_info( + &metadata, + eval_windows_file_info(&path), + context.local_file_mode(&path), + values, + ) } /// Converts filesystem metadata into PHP's numeric-and-string keyed stat array. @@ -137,6 +171,7 @@ pub(in crate::interpreter) fn eval_stat_metadata_array( metadata: &std::fs::Metadata, values: &mut impl RuntimeValueOps, ) -> Result { + #[cfg(unix)] let fields = [ ("dev", eval_u64_to_i64(metadata.dev())?), ("ino", eval_u64_to_i64(metadata.ino())?), @@ -152,12 +187,155 @@ pub(in crate::interpreter) fn eval_stat_metadata_array( ("blksize", eval_u64_to_i64(metadata.blksize())?), ("blocks", eval_u64_to_i64(metadata.blocks())?), ]; + #[cfg(windows)] + return eval_stat_metadata_array_with_windows_info(metadata, None, None, values); + #[cfg(unix)] + { let mut result = values.assoc_new(fields.len() * 2)?; for (index, (name, value)) in fields.iter().enumerate() { result = eval_stat_array_set_int_key(result, index, *value, values)?; result = eval_stat_array_set_string_key(result, name, *value, values)?; } Ok(result) + } +} + +/// Converts Windows metadata and optional handle identity into PHP's stat array. +#[cfg(windows)] +fn eval_stat_metadata_array_with_windows_info( + metadata: &std::fs::Metadata, + info: Option, + mode_override: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let (device, inode, links) = info + .map(|info| { + ( + i64::from(info.volume_serial), + i64::try_from(info.file_index).unwrap_or(0), + i64::from(info.number_of_links), + ) + }) + .unwrap_or((0, 0, 1)); + let physical_mode = eval_windows_metadata_mode(metadata); + let mode = mode_override + .map(|permissions| (physical_mode & !0o7777) | i64::from(permissions)) + .unwrap_or(physical_mode); + let fields = [ + ("dev", device), + ("ino", inode), + ("mode", mode), + ("nlink", links), + ("uid", 0), + ("gid", 0), + ("rdev", 0), + ("size", eval_u64_to_i64(metadata.file_size())?), + ("atime", eval_windows_filetime_seconds(metadata.last_access_time())), + ("mtime", eval_windows_filetime_seconds(metadata.last_write_time())), + ("ctime", eval_windows_filetime_seconds(metadata.creation_time())), + ("blksize", 0), + ("blocks", 0), + ]; + let mut result = values.assoc_new(fields.len() * 2)?; + for (index, (name, value)) in fields.iter().enumerate() { + result = eval_stat_array_set_int_key(result, index, *value, values)?; + result = eval_stat_array_set_string_key(result, name, *value, values)?; + } + Ok(result) +} + +/// Stable file identity fields exposed by `GetFileInformationByHandle`. +#[cfg(windows)] +#[derive(Clone, Copy)] +struct WindowsFileInfo { + volume_serial: u32, + number_of_links: u32, + file_index: u64, +} + +/// Reads stable Windows volume, link-count, and file-index metadata for a path. +#[cfg(windows)] +fn eval_windows_file_info(path: &str) -> Option { + use std::ffi::c_void; + use std::os::windows::io::AsRawHandle; + + #[repr(C)] + struct FileTime { + low: u32, + high: u32, + } + + #[repr(C)] + struct ByHandleFileInformation { + attributes: u32, + creation_time: FileTime, + last_access_time: FileTime, + last_write_time: FileTime, + volume_serial: u32, + file_size_high: u32, + file_size_low: u32, + number_of_links: u32, + file_index_high: u32, + file_index_low: u32, + } + + #[link(name = "kernel32")] + unsafe extern "system" { + /// Reads filesystem identity and timestamps for one open Windows file handle. + fn GetFileInformationByHandle( + file: *mut c_void, + information: *mut ByHandleFileInformation, + ) -> i32; + } + + let file = std::fs::File::open(path).ok()?; + let mut information = std::mem::MaybeUninit::::uninit(); + let status = unsafe { + GetFileInformationByHandle(file.as_raw_handle(), information.as_mut_ptr()) + }; + if status == 0 { + return None; + } + let information = unsafe { information.assume_init() }; + Some(WindowsFileInfo { + volume_serial: information.volume_serial, + number_of_links: information.number_of_links, + file_index: (u64::from(information.file_index_high) << 32) + | u64::from(information.file_index_low), + }) +} + +/// Synthesizes PHP's POSIX-shaped mode bits from Windows metadata. +#[cfg(windows)] +fn eval_windows_metadata_mode(metadata: &std::fs::Metadata) -> i64 { + const FILE_ATTRIBUTE_READONLY: u32 = 0x0000_0001; + const S_IFDIR: i64 = 0o040000; + const S_IFLNK: i64 = 0o120000; + const S_IFREG: i64 = 0o100000; + let kind = if metadata.file_type().is_symlink() { + S_IFLNK + } else if metadata.is_dir() { + S_IFDIR + } else { + S_IFREG + }; + let permissions = if metadata.file_attributes() & FILE_ATTRIBUTE_READONLY != 0 { + 0o444 + } else { + 0o666 + }; + kind | permissions +} + +/// Converts a Windows FILETIME count to whole Unix epoch seconds. +#[cfg(windows)] +fn eval_windows_filetime_seconds(filetime: u64) -> i64 { + const WINDOWS_TO_UNIX_EPOCH_100NS: u64 = 116_444_736_000_000_000; + filetime + .saturating_sub(WINDOWS_TO_UNIX_EPOCH_100NS) + .checked_div(10_000_000) + .and_then(|seconds| i64::try_from(seconds).ok()) + .unwrap_or(0) } /// Inserts one integer stat field under a numeric PHP array key. diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/sys_get_temp_dir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/sys_get_temp_dir.rs index 789b1d91c2..3a348f0c2b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/sys_get_temp_dir.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/sys_get_temp_dir.rs @@ -50,9 +50,48 @@ pub(in crate::interpreter) fn eval_builtin_sys_get_temp_dir( eval_sys_get_temp_dir_result(values) } -/// Returns the same temporary directory literal as the native static builtin. +/// Returns the process temporary directory, matching the compiled builtin. +/// +/// On Windows the compiled path calls `__rt_sys_get_temp_dir` (GetTempPathW-backed), +/// so returning the literal `/tmp` here made an eval fragment contradict the rest of +/// its own binary with a path that is not even valid on that platform. +/// +/// The POSIX arm resolves `TMPDIR` exactly as the compiled runtime helper +/// `__rt_php_temp_dir` does, so an eval fragment and the binary around it always +/// name the same directory. pub(in crate::interpreter) fn eval_sys_get_temp_dir_result( values: &mut impl RuntimeValueOps, ) -> Result { - values.string("/tmp") + values.string(&eval_temp_dir()) +} + +/// Resolves the temporary directory for the platform this interpreter targets. +pub(in crate::interpreter) fn eval_temp_dir() -> String { + if cfg!(target_os = "windows") { + // php strips the trailing separator GetTempPath() always appends; the TMP / + // TEMP / USERPROFILE order mirrors what the Win32 call itself consults. + for key in ["TMP", "TEMP", "USERPROFILE"] { + if let Ok(dir) = std::env::var(key) { + let trimmed = dir.trim_end_matches(['\\', '/']); + if !trimmed.is_empty() { + return trimmed.to_string(); + } + } + } + return "C:\\Windows\\Temp".to_string(); + } + + // php reads TMPDIR first and drops **exactly one** trailing slash, so + // "/var/tmp/probe///" resolves to "/var/tmp/probe//" and a bare "/" resolves to + // the empty string. Only an unset or empty TMPDIR falls back to P_tmpdir, which + // php returns verbatim -- trailing slash included on macOS. + if let Ok(dir) = std::env::var("TMPDIR") { + if !dir.is_empty() { + return dir.strip_suffix('/').unwrap_or(&dir).to_string(); + } + } + if cfg!(target_os = "macos") { + return "/var/tmp/".to_string(); + } + "/tmp".to_string() } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/umask.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/umask.rs index 3b2f1c97c0..d68a39c64b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/umask.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/umask.rs @@ -67,13 +67,13 @@ pub(in crate::interpreter) fn eval_umask_result( let previous = match mask { Some(mask) => { let mask = eval_int_value(mask, values)? as u32; - unsafe { umask(mask) } + eval_os_umask(mask) } - None => unsafe { - let current = umask(0); - umask(current); + None => { + let current = eval_os_umask(0); + eval_os_umask(current); current - }, + } }; values.int(i64::from(previous)) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/unlink.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/unlink.rs index c0856464de..2377d54e8e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/unlink.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/unlink.rs @@ -71,5 +71,12 @@ pub(in crate::interpreter) fn eval_unlink_result( let Some(path) = stream_wrappers::local_filesystem_path(&path) else { return values.bool_value(false); }; - values.bool_value(std::fs::remove_file(path).is_ok()) + #[cfg(windows)] + let removed_mode = context.capture_local_file_mode(&path); + let removed = std::fs::remove_file(&path).is_ok(); + #[cfg(windows)] + if removed { + context.unlink_local_file_mode(removed_mode); + } + values.bool_value(removed) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_stat.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_stat.rs index a96813d125..0e91fe555f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_stat.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_stat.rs @@ -13,6 +13,15 @@ use super::super::super::*; +const EVAL_S_IFMT: i64 = 0o170000; +const EVAL_S_IFIFO: i64 = 0o010000; +const EVAL_S_IFCHR: i64 = 0o020000; +const EVAL_S_IFDIR: i64 = 0o040000; +const EVAL_S_IFBLK: i64 = 0o060000; +const EVAL_S_IFREG: i64 = 0o100000; +const EVAL_S_IFLNK: i64 = 0o120000; +const EVAL_S_IFSOCK: i64 = 0o140000; + /// Dispatches `fstat()` to a wrapper object's `stream_stat()`. pub(in crate::interpreter) fn eval_user_wrapper_fstat_result( id: i64, @@ -51,10 +60,10 @@ pub(in crate::interpreter) fn eval_user_wrapper_file_probe_from_stat( let mode = eval_user_wrapper_stat_int_field(stat, "mode", values)?.unwrap_or(0); let result = match name { "file_exists" => true, - "is_dir" => eval_mode_kind(mode) == libc::S_IFDIR as i64, + "is_dir" => eval_mode_kind(mode) == EVAL_S_IFDIR, "is_executable" => mode & 0o111 != 0, - "is_file" => eval_mode_kind(mode) == libc::S_IFREG as i64, - "is_link" => eval_mode_kind(mode) == libc::S_IFLNK as i64, + "is_file" => eval_mode_kind(mode) == EVAL_S_IFREG, + "is_link" => eval_mode_kind(mode) == EVAL_S_IFLNK, "is_readable" => mode & 0o444 != 0, "is_writable" | "is_writeable" => mode & 0o222 != 0, _ => return Err(EvalStatus::RuntimeFatal), @@ -102,18 +111,18 @@ pub(in crate::interpreter) fn eval_user_wrapper_stat_int_field( /// Maps one POSIX mode value to PHP's `filetype()` label. pub(in crate::interpreter) fn eval_filetype_label_from_mode(mode: i64) -> &'static str { match eval_mode_kind(mode) { - kind if kind == libc::S_IFREG as i64 => "file", - kind if kind == libc::S_IFDIR as i64 => "dir", - kind if kind == libc::S_IFLNK as i64 => "link", - kind if kind == libc::S_IFCHR as i64 => "char", - kind if kind == libc::S_IFBLK as i64 => "block", - kind if kind == libc::S_IFIFO as i64 => "fifo", - kind if kind == libc::S_IFSOCK as i64 => "socket", + kind if kind == EVAL_S_IFREG => "file", + kind if kind == EVAL_S_IFDIR => "dir", + kind if kind == EVAL_S_IFLNK => "link", + kind if kind == EVAL_S_IFCHR => "char", + kind if kind == EVAL_S_IFBLK => "block", + kind if kind == EVAL_S_IFIFO => "fifo", + kind if kind == EVAL_S_IFSOCK => "socket", _ => "unknown", } } /// Masks one POSIX mode value down to its file-kind bits. fn eval_mode_kind(mode: i64) -> i64 { - mode & (libc::S_IFMT as i64) + mode & EVAL_S_IFMT } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/values_dispatch.rs index 6eef8321e4..ae44de3caa 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/values_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/values_dispatch.rs @@ -85,6 +85,10 @@ pub(in crate::interpreter) fn eval_filesystem_values_result( "pclose" => super::pclose::eval_pclose_declared_values_result(evaluated_args, context, values), "pfsockopen" => super::pfsockopen::eval_pfsockopen_declared_values_result(evaluated_args, context, values), "popen" => super::popen::eval_popen_declared_values_result(evaluated_args, context, values), + "proc_close" => super::proc_close::eval_proc_close_declared_values_result(evaluated_args, context, values), + "proc_get_status" => super::proc_get_status::eval_proc_get_status_declared_values_result(evaluated_args, context, values), + "proc_open" => super::proc_open::eval_proc_open_declared_values_result(evaluated_args, context, values), + "proc_terminate" => super::proc_terminate::eval_proc_terminate_declared_values_result(evaluated_args, context, values), "readdir" => super::readdir::eval_readdir_declared_values_result(evaluated_args, context, values), "readfile" => super::readfile::eval_readfile_declared_values_result(evaluated_args, context, values), "readline" => super::readline::eval_readline_declared_values_result(evaluated_args, context, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index 259b270ca1..e61e840b91 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -198,6 +198,8 @@ pub(in crate::interpreter) enum EvalDirectHook { Rad2deg, /// Dispatches `rand(...)`. Rand, + /// Dispatches `random_bytes(...)`. + RandomBytes, /// Dispatches `random_int(...)`. RandomInt, /// Dispatches `round(...)`. @@ -446,6 +448,7 @@ impl EvalDirectHook { Self::Pow => eval_builtin_pow(args, context, scope, values), Self::Rad2deg => eval_builtin_rad2deg(args, context, scope, values), Self::Rand => eval_builtin_rand(args, context, scope, values), + Self::RandomBytes => eval_builtin_random_bytes(args, context, scope, values), Self::RandomInt => eval_builtin_random_int(args, context, scope, values), Self::Round => eval_builtin_round(args, context, scope, values), Self::MbEregMatch => eval_builtin_mb_ereg_match(args, context, scope, values), @@ -478,6 +481,8 @@ impl EvalDirectHook { Self::Sinh => eval_builtin_sinh(args, context, scope, values), Self::Slashes => match name { "addslashes" => eval_builtin_addslashes(args, context, scope, values), + "escapeshellarg" => eval_builtin_escapeshellarg(args, context, scope, values), + "escapeshellcmd" => eval_builtin_escapeshellcmd(args, context, scope, values), "stripslashes" => eval_builtin_stripslashes(args, context, scope, values), _ => Err(EvalStatus::RuntimeFatal), }, diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index bb693880dd..f0c65c5b33 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -201,6 +201,8 @@ pub(in crate::interpreter) enum EvalValuesHook { Rad2deg, /// Dispatches `rand(...)`. Rand, + /// Dispatches `random_bytes(...)`. + RandomBytes, /// Dispatches `random_int(...)`. RandomInt, /// Dispatches `round(...)`. @@ -492,6 +494,7 @@ impl EvalValuesHook { Self::Pow => two_args(evaluated_args, values, eval_pow_result), Self::Rad2deg => one_arg(evaluated_args, values, eval_rad2deg_result), Self::Rand => eval_rand_values_result(evaluated_args, values), + Self::RandomBytes => eval_random_bytes_values_result(evaluated_args, values), Self::RandomInt => eval_random_int_values_result(evaluated_args, values), Self::Round => match evaluated_args { [value] => eval_round_result(*value, None, values), @@ -529,6 +532,8 @@ impl EvalValuesHook { Self::Sinh => one_arg(evaluated_args, values, eval_sinh_result), Self::Slashes => one_arg(evaluated_args, values, |value, values| match name { "addslashes" => eval_addslashes_result(value, values), + "escapeshellarg" => eval_escapeshellarg_result(value, values), + "escapeshellcmd" => eval_escapeshellcmd_result(value, values), "stripslashes" => eval_stripslashes_result(value, values), _ => Err(EvalStatus::RuntimeFatal), }), diff --git a/crates/elephc-magician/src/interpreter/builtins/math/mod.rs b/crates/elephc-magician/src/interpreter/builtins/math/mod.rs index ac4de3582a..0443565c0d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/mod.rs @@ -33,6 +33,7 @@ mod mt_rand; mod pi; mod pow; mod rand; +mod random_bytes; mod random_int; mod rad2deg; mod round; @@ -68,6 +69,7 @@ pub(in crate::interpreter) use pi::*; pub(in crate::interpreter) use pow::*; pub(in crate::interpreter) use rad2deg::*; pub(in crate::interpreter) use rand::*; +pub(in crate::interpreter) use random_bytes::*; pub(in crate::interpreter) use random_int::*; pub(in crate::interpreter) use round::*; pub(in crate::interpreter) use sin::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/math/random_bytes.rs b/crates/elephc-magician/src/interpreter/builtins/math/random_bytes.rs new file mode 100644 index 0000000000..f32a1340f8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/random_bytes.rs @@ -0,0 +1,58 @@ +//! Purpose: +//! Eval registry entry and implementation for `random_bytes`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Bytes come directly from the operating system CSPRNG and remain binary-safe. + +use super::super::super::*; + +eval_builtin! { + name: "random_bytes", + area: Math, + params: [length], + direct: RandomBytes, + values: RandomBytes, +} + +/// Evaluates PHP `random_bytes()` with one strictly positive byte length. +pub(in crate::interpreter) fn eval_builtin_random_bytes( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [length] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let length = eval_expr(length, context, scope, values)?; + eval_random_bytes_result(length, values) +} + +/// Dispatches a by-value `random_bytes()` call after argument binding. +pub(in crate::interpreter) fn eval_random_bytes_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [length] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_random_bytes_result(*length, values) +} + +/// Returns a binary-safe PHP string filled by the operating system CSPRNG. +pub(in crate::interpreter) fn eval_random_bytes_result( + length: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let length = eval_int_value(length, values)?; + let length = usize::try_from(length) + .ok() + .filter(|length| *length > 0) + .ok_or(EvalStatus::RuntimeFatal)?; + let mut bytes = vec![0_u8; length]; + getrandom::getrandom(&mut bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + values.string_bytes_value(&bytes) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/random_int.rs b/crates/elephc-magician/src/interpreter/builtins/math/random_int.rs index 0bc4846bdf..488c5627ab 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/random_int.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/random_int.rs @@ -55,9 +55,13 @@ pub(in crate::interpreter) fn eval_random_int_result( if min > max { return Err(EvalStatus::RuntimeFatal); } - let width = (i128::from(max) - i128::from(min) + 1) as u128; - let offset = (eval_random_u128() % width) as i128; - let sampled = i128::from(min) + offset; + // Inclusive width, so the full PHP integer range stays representable: max - min + // is UINT64_MAX for random_int(PHP_INT_MIN, PHP_INT_MAX), where max - min + 1 + // would wrap. The CSPRNG draw is rejection-sampled, matching PHP's guarantee + // that random_int() is both cryptographically secure and unbiased. + let umax = (i128::from(max) - i128::from(min)) as u128 as u64; + let offset = eval_csprng_range(umax)?; + let sampled = i128::from(min) + i128::from(offset); let sampled = i64::try_from(sampled).map_err(|_| EvalStatus::RuntimeFatal)?; values.int(sampled) } diff --git a/crates/elephc-magician/src/interpreter/builtins/mod.rs b/crates/elephc-magician/src/interpreter/builtins/mod.rs index 25e111e44b..80f7c009b0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/mod.rs @@ -17,7 +17,7 @@ mod macros; mod array; mod class_metadata; mod core; -mod filesystem; +pub(in crate::interpreter) mod filesystem; mod formatting; mod hooks; mod json; diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyaddr.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyaddr.rs index 751d9f28d7..9c8c0d6bc0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyaddr.rs +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyaddr.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. //! //! Key details: -//! - libc resolver storage is copied before any subsequent resolver lookup can overwrite it. +//! - OS resolver storage is copied before any subsequent resolver lookup can overwrite it. use super::*; @@ -41,21 +41,7 @@ pub(in crate::interpreter) fn eval_gethostbyaddr_result( let Ok(ipv4) = ip_text.parse::() else { return values.bool_value(false); }; - let octets = ipv4.octets(); - let resolved = unsafe { - // libc reads the stack-owned IPv4 octets during this call and returns - // static resolver storage, which is copied before the next resolver call. - let host = libc_gethostbyaddr( - octets.as_ptr().cast::(), - octets.len() as libc::socklen_t, - libc::AF_INET, - ); - if host.is_null() || (*host).h_name.is_null() { - None - } else { - Some(CStr::from_ptr((*host).h_name).to_bytes().to_vec()) - } - }; + let resolved = eval_reverse_ipv4_name(ipv4.octets()); match resolved { Some(name) if !name.is_empty() => values.string_bytes_value(&name), _ => values.string(ip_text.as_ref()), diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/gethostname.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/gethostname.rs index 2b4a545e60..bef5b3475f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/gethostname.rs +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/gethostname.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. //! //! Key details: -//! - The libc hostname buffer is stack-owned and copied into a PHP string. +//! - The OS hostname buffer is copied into a PHP string before returning. use super::*; @@ -28,25 +28,12 @@ pub(in crate::interpreter) fn eval_builtin_gethostname( eval_gethostname_result(values) } -/// Reads the current host name through libc and returns an empty string on failure. +/// Reads the current host name through the platform API and returns an empty string on failure. pub(in crate::interpreter) fn eval_gethostname_result( values: &mut impl RuntimeValueOps, ) -> Result { - let mut buffer = [0 as libc::c_char; 256]; - let status = unsafe { - // libc writes at most buffer.len() bytes into this stack buffer. - libc::gethostname(buffer.as_mut_ptr(), buffer.len()) - }; - if status != 0 { - return values.string(""); + match eval_os_hostname() { + Some(hostname) => values.string_bytes_value(&hostname), + None => values.string(""), } - let length = buffer - .iter() - .position(|byte| *byte == 0) - .unwrap_or(buffer.len()); - let hostname = buffer[..length] - .iter() - .map(|byte| *byte as u8) - .collect::>(); - values.string_bytes_value(&hostname) } diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobyname.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobyname.rs index 1a1982e1e9..7bbcb34835 100644 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobyname.rs +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobyname.rs @@ -39,15 +39,10 @@ pub(in crate::interpreter) fn eval_getprotobyname_result( let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { return values.bool_value(false); }; - let entry = unsafe { - // libc returns a process-global protoent; copy scalar fields before another lookup. - libc_getprotobyname(protocol.as_ptr()) - }; - if entry.is_null() { - return values.bool_value(false); + match eval_protocol_number(&protocol) { + Some(number) => values.int(i64::from(number)), + None => values.bool_value(false), } - let number = unsafe { (*entry).p_proto }; - values.int(i64::from(number)) } @@ -64,20 +59,13 @@ pub(in crate::interpreter) fn eval_lowercase_c_string( Ok(CString::new(bytes).ok()) } -/// Copies a protoent canonical name into a PHP string or returns PHP false. +/// Copies a protocol canonical name into a PHP string or returns PHP false. pub(in crate::interpreter) fn eval_protoent_name_or_false( - entry: *mut libc::protoent, + name: Option>, values: &mut impl RuntimeValueOps, ) -> Result { - if entry.is_null() { - return values.bool_value(false); + match name { + Some(name) => values.string_bytes_value(&name), + None => values.bool_value(false), } - let name = unsafe { - let name = (*entry).p_name; - if name.is_null() { - return values.bool_value(false); - } - CStr::from_ptr(name).to_bytes().to_vec() - }; - values.string_bytes_value(&name) } diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobynumber.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobynumber.rs index 0b71fb38bc..64f7a6e037 100644 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobynumber.rs +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobynumber.rs @@ -40,9 +40,5 @@ pub(in crate::interpreter) fn eval_getprotobynumber_result( let Ok(protocol) = libc::c_int::try_from(protocol) else { return values.bool_value(false); }; - let entry = unsafe { - // libc returns a process-global protoent; copy the name before another lookup. - libc_getprotobynumber(protocol) - }; - eval_protoent_name_or_false(entry, values) + eval_protoent_name_or_false(eval_protocol_name(protocol), values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyname.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyname.rs index 478e58b48d..8b2decd244 100644 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyname.rs +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyname.rs @@ -44,32 +44,20 @@ pub(in crate::interpreter) fn eval_getservbyname_result( let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { return values.bool_value(false); }; - let entry = unsafe { - // libc returns a process-global servent; copy scalar fields before another lookup. - libc_getservbyname(service.as_ptr(), protocol.as_ptr()) - }; - if entry.is_null() { - return values.bool_value(false); + match eval_service_port(&service, &protocol) { + Some(port) => values.int(i64::from(port)), + None => values.bool_value(false), } - let port = unsafe { u16::from_be((*entry).s_port as u16) }; - values.int(i64::from(port)) } -/// Copies a servent canonical name into a PHP string or returns PHP false. +/// Copies a service canonical name into a PHP string or returns PHP false. pub(in crate::interpreter) fn eval_servent_name_or_false( - entry: *mut libc::servent, + name: Option>, values: &mut impl RuntimeValueOps, ) -> Result { - if entry.is_null() { - return values.bool_value(false); + match name { + Some(name) => values.string_bytes_value(&name), + None => values.bool_value(false), } - let name = unsafe { - let name = (*entry).s_name; - if name.is_null() { - return values.bool_value(false); - } - CStr::from_ptr(name).to_bytes().to_vec() - }; - values.string_bytes_value(&name) } diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyport.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyport.rs index b3adc46ae3..f84f34e4a5 100644 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyport.rs +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyport.rs @@ -45,10 +45,5 @@ pub(in crate::interpreter) fn eval_getservbyport_result( let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { return values.bool_value(false); }; - let network_port = port.to_be() as libc::c_int; - let entry = unsafe { - // libc returns a process-global servent; copy the name before another lookup. - libc_getservbyport(network_port, protocol.as_ptr()) - }; - eval_servent_name_or_false(entry, values) + eval_servent_name_or_false(eval_service_name(port, &protocol), values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/php_uname.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/php_uname.rs index 27bc934606..db4b362100 100644 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/php_uname.rs +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/php_uname.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. //! //! Key details: -//! - libc uname fields are copied into PHP strings before formatting the requested mode. +//! - Platform uname fields are copied into PHP strings before formatting the requested mode. use super::*; @@ -52,28 +52,20 @@ pub(in crate::interpreter) fn eval_php_uname_result( None => b'a', }; - let mut utsname = std::mem::MaybeUninit::::zeroed(); - let status = unsafe { - // libc writes all uname fields into the stack-owned utsname buffer. - libc::uname(utsname.as_mut_ptr()) - }; - if status != 0 { + let Some(uname) = eval_os_uname() else { return values.string(""); - } - let utsname = unsafe { - // `uname` succeeded, so libc initialized the full `utsname` structure. - utsname.assume_init() }; - let sysname = eval_uname_field_bytes(&utsname.sysname); - let nodename = eval_uname_field_bytes(&utsname.nodename); - let release = eval_uname_field_bytes(&utsname.release); - let version = eval_uname_field_bytes(&utsname.version); - let machine = eval_uname_field_bytes(&utsname.machine); match mode { b'a' => { let mut output = Vec::new(); - for field in [&sysname, &nodename, &release, &version, &machine] { + for field in [ + &uname.sysname, + &uname.nodename, + &uname.release, + &uname.version, + &uname.machine, + ] { if !output.is_empty() { output.push(b' '); } @@ -81,20 +73,11 @@ pub(in crate::interpreter) fn eval_php_uname_result( } values.string_bytes_value(&output) } - b's' => values.string_bytes_value(&sysname), - b'n' => values.string_bytes_value(&nodename), - b'r' => values.string_bytes_value(&release), - b'v' => values.string_bytes_value(&version), - b'm' => values.string_bytes_value(&machine), + b's' => values.string_bytes_value(&uname.sysname), + b'n' => values.string_bytes_value(&uname.nodename), + b'r' => values.string_bytes_value(&uname.release), + b'v' => values.string_bytes_value(&uname.version), + b'm' => values.string_bytes_value(&uname.machine), _ => Err(EvalStatus::RuntimeFatal), } } - -/// Copies one NUL-terminated `utsname` field into raw PHP string bytes. -pub(in crate::interpreter) fn eval_uname_field_bytes(field: &[libc::c_char]) -> Vec { - let length = field - .iter() - .position(|byte| *byte == 0) - .unwrap_or(field.len()); - field[..length].iter().map(|byte| *byte as u8).collect() -} diff --git a/crates/elephc-magician/src/interpreter/builtins/random.rs b/crates/elephc-magician/src/interpreter/builtins/random.rs index ef646be040..84833ee72e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/random.rs +++ b/crates/elephc-magician/src/interpreter/builtins/random.rs @@ -6,8 +6,12 @@ //! - `crate::interpreter::builtins::array` randomizing builtins. //! //! Key details: -//! - This is eval-local, process-local, and non-cryptographic; PHP-visible -//! builtin owners decide range and key semantics. +//! - `eval_random_u128` is eval-local, process-local, and non-cryptographic; it +//! backs `rand`, `mt_rand`, `shuffle` and `array_rand`, which PHP itself does not +//! promise to make cryptographically secure. +//! - `random_int` is different: PHP guarantees a CSPRNG and raises when none is +//! available, so it goes through `eval_csprng_range`, which draws from the OS +//! entropy source and rejection-samples to stay unbiased. use super::super::*; @@ -26,3 +30,38 @@ pub(in crate::interpreter) fn eval_random_u128() -> u128 { value = value.wrapping_mul(0x94d0_49bb_1331_11eb); value ^ (value >> 31) } + +/// Draws one uint64 from the operating system's CSPRNG. +/// +/// PHP's `random_int()` is specified to fail rather than fall back to a weaker +/// source, so an unavailable entropy source becomes a runtime fatal here instead +/// of silently degrading to `eval_random_u128`. +fn eval_csprng_u64() -> Result { + let mut bytes = [0u8; 8]; + getrandom::getrandom(&mut bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(u64::from_le_bytes(bytes)) +} + +/// Returns a uniform value in `[0, umax]` drawn from the OS CSPRNG. +/// +/// Mirrors php-src `php_random_range64` (ext/random/random.c): `UINT64_MAX` is +/// returned unreduced, a power-of-two count is masked, and anything else is +/// rejection-sampled against `UINT64_MAX - (UINT64_MAX % count) - 1`. A plain +/// `draw % count` would be modulo-biased toward the low end of the range, which is +/// exactly what PHP takes care to avoid. +pub(in crate::interpreter) fn eval_csprng_range(umax: u64) -> Result { + if umax == u64::MAX { + return eval_csprng_u64(); + } + let count = umax + 1; + if count.is_power_of_two() { + return Ok(eval_csprng_u64()? & (count - 1)); + } + let limit = u64::MAX - (u64::MAX % count) - 1; + loop { + let candidate = eval_csprng_u64()?; + if candidate <= limit { + return Ok(candidate % count); + } + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/tests/direct_hooks.rs b/crates/elephc-magician/src/interpreter/builtins/registry/tests/direct_hooks.rs index fe28c54bc5..46298a8627 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/tests/direct_hooks.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/tests/direct_hooks.rs @@ -40,6 +40,7 @@ use super::*; "natcasesort", "natsort", "pfsockopen", + "proc_open", "rsort", "settype", "shuffle", diff --git a/crates/elephc-magician/src/interpreter/builtins/string/escapeshellarg.rs b/crates/elephc-magician/src/interpreter/builtins/string/escapeshellarg.rs new file mode 100644 index 0000000000..3ebd96ea37 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/escapeshellarg.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declares the eval-visible `escapeshellarg` builtin. +//! +//! Called from: +//! - The registry's shared Slashes hook. +//! +//! Key details: +//! - The leaf delegates platform-specific escaping to `shell_escape`. + +eval_builtin! { + name: "escapeshellarg", + area: String, + params: [arg], + direct: Slashes, + values: Slashes, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/escapeshellcmd.rs b/crates/elephc-magician/src/interpreter/builtins/string/escapeshellcmd.rs new file mode 100644 index 0000000000..d6b8c2d2a7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/escapeshellcmd.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declares the eval-visible `escapeshellcmd` builtin. +//! +//! Called from: +//! - The registry's shared Slashes hook. +//! +//! Key details: +//! - The leaf delegates platform-specific escaping to `shell_escape`. + +eval_builtin! { + name: "escapeshellcmd", + area: String, + params: [command], + direct: Slashes, + values: Slashes, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/mb_strlen.rs b/crates/elephc-magician/src/interpreter/builtins/string/mb_strlen.rs index c748c3823c..61febfcc11 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/mb_strlen.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/mb_strlen.rs @@ -26,7 +26,11 @@ eval_builtin! { #[cfg_attr(target_os = "macos", link(name = "iconv"))] unsafe extern "C" { + /// Opens an iconv conversion descriptor for the requested encoding pair. + #[cfg_attr(windows, link_name = "libiconv_open")] fn iconv_open(tocode: *const c_char, fromcode: *const c_char) -> *mut c_void; + /// Converts an input byte buffer through an open iconv descriptor. + #[cfg_attr(windows, link_name = "libiconv")] fn iconv( cd: *mut c_void, inbuf: *mut *mut c_char, @@ -34,6 +38,8 @@ unsafe extern "C" { outbuf: *mut *mut c_char, outbytesleft: *mut usize, ) -> usize; + /// Closes an iconv conversion descriptor and releases its native resources. + #[cfg_attr(windows, link_name = "libiconv_close")] fn iconv_close(cd: *mut c_void) -> i32; } diff --git a/crates/elephc-magician/src/interpreter/builtins/string/mod.rs b/crates/elephc-magician/src/interpreter/builtins/string/mod.rs index 82e8bf27e1..97704452c4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/mod.rs @@ -21,6 +21,8 @@ mod ctype_alpha; mod ctype_digit; mod ctype_space; mod explode; +mod escapeshellarg; +mod escapeshellcmd; mod grapheme_strrev; mod gzcompress; mod gzdeflate; @@ -50,6 +52,7 @@ mod rawurldecode; mod rawurlencode; mod rtrim; mod sha1; +mod shell_escape; mod str_contains; mod str_ends_with; mod str_ireplace; @@ -94,6 +97,7 @@ pub(in crate::interpreter) use ctype_alpha::*; pub(in crate::interpreter) use ctype_digit::*; pub(in crate::interpreter) use ctype_space::*; pub(in crate::interpreter) use explode::*; +pub(in crate::interpreter) use shell_escape::*; pub(in crate::interpreter) use grapheme_strrev::*; pub(in crate::interpreter) use gzcompress::*; pub(in crate::interpreter) use gzdeflate::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/string/shell_escape.rs b/crates/elephc-magician/src/interpreter/builtins/string/shell_escape.rs new file mode 100644 index 0000000000..2ffd69055a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/shell_escape.rs @@ -0,0 +1,230 @@ +//! Purpose: +//! Implements eval-time PHP shell escaping for argument and command fragments. +//! +//! Called from: +//! - The builtin hook dispatch for `escapeshellarg()` and `escapeshellcmd()`. +//! +//! Key details: +//! - NUL is rejected before shell syntax is produced. +//! - Windows and POSIX use deliberately distinct quoting rules. + +use super::super::super::*; + +/// Evaluates a direct `escapeshellarg(arg)` call before platform-specific escaping. +pub(in crate::interpreter) fn eval_builtin_escapeshellarg( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_escapeshellarg_result(value, values) +} + +/// Evaluates a direct `escapeshellcmd(command)` call before platform-specific escaping. +pub(in crate::interpreter) fn eval_builtin_escapeshellcmd( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_escapeshellcmd_result(value, values) +} + +/// Escapes one string as a single shell argument using the host platform's PHP rules. +pub(in crate::interpreter) fn eval_escapeshellarg_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + if bytes.contains(&0) { + return Err(EvalStatus::RuntimeFatal); + } + #[cfg(windows)] + let escaped = eval_windows_shell_arg(&bytes); + #[cfg(not(windows))] + let escaped = eval_posix_shell_arg(&bytes); + values.string_bytes_value(&escaped) +} + +/// Escapes one string for interpolation into a shell command using host-specific metacharacters. +pub(in crate::interpreter) fn eval_escapeshellcmd_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + if bytes.contains(&0) { + return Err(EvalStatus::RuntimeFatal); + } + #[cfg(windows)] + let escaped = eval_windows_shell_cmd(&bytes); + #[cfg(not(windows))] + let escaped = eval_posix_shell_cmd(&bytes); + values.string_bytes_value(&escaped) +} + +/// Quotes one POSIX shell argument while preserving embedded apostrophes. +#[cfg(not(windows))] +fn eval_posix_shell_arg(bytes: &[u8]) -> Vec { + let mut escaped = Vec::with_capacity(bytes.len() + 2); + escaped.push(b'\''); + for &byte in bytes { + if byte == 0xff { + continue; + } + if byte == b'\'' { + escaped.extend_from_slice(b"'\\''"); + } else { + escaped.push(byte); + } + } + escaped.push(b'\''); + escaped +} + +/// Quotes one Windows command-shell argument, neutralizing delayed-expansion syntax. +/// +/// Compiled on every host so the quoting rules stay unit-testable; only the +/// Windows build routes `escapeshellarg()` through it, hence the dead-code +/// allowance elsewhere. +#[cfg_attr(not(windows), allow(dead_code))] +pub(super) fn eval_windows_shell_arg(bytes: &[u8]) -> Vec { + let mut escaped = Vec::with_capacity(bytes.len() + 2); + let mut trailing_backslashes = 0_usize; + escaped.push(b'"'); + for &byte in bytes { + if byte == 0xff { + continue; + } + if byte == b'\\' { + trailing_backslashes += 1; + } else { + trailing_backslashes = 0; + } + if matches!(byte, b'"' | b'%' | b'!') { + // php REPLACES these bytes with a space and does not re-emit them: + // ext/standard/exec.c php_escape_shell_arg breaks out of the switch + // before the `default` arm that would append the original byte. + // Emitting both let a '"' close the quoting early, so + // escapeshellarg('a"b') produced "a "b" and the tail escaped the + // argument -- the injection this quoting exists to prevent. + escaped.push(b' '); + continue; + } + escaped.push(byte); + } + if trailing_backslashes % 2 == 1 { + escaped.push(b'\\'); + } + escaped.push(b'"'); + escaped +} + +/// Backslash-escapes POSIX shell metacharacters without adding argument quotes. +#[cfg(not(windows))] +fn eval_posix_shell_cmd(bytes: &[u8]) -> Vec { + eval_posix_shell_cmd_with_quote_pairs(bytes) +} + +/// Caret-escapes Windows command-shell metacharacters without adding argument quotes. +#[cfg(windows)] +fn eval_windows_shell_cmd(bytes: &[u8]) -> Vec { + eval_windows_shell_cmd_with_escape(bytes, b'^') +} + +/// Applies Windows command-shell metacharacter escaping with a caret prefix. +#[cfg(windows)] +fn eval_windows_shell_cmd_with_escape(bytes: &[u8], escape: u8) -> Vec { + let mut escaped = Vec::with_capacity(bytes.len() * 2); + for &byte in bytes { + if byte == 0xff { + continue; + } + let special = matches!(byte, b'#' | b'&' | b';' | b'`' | b'|' | b'*' | b'?' | b'~' | b'<' | b'>' | b'^' | b'(' | b')' | b'[' | b']' | b'{' | b'}' | b'$' | b'\\' | b'\n'); + if special || matches!(byte, b'%' | b'!' | b'"' | b'\'') { + escaped.push(escape); + } + escaped.push(byte); + } + escaped +} + +/// Backslash-escapes POSIX command metacharacters while retaining matched quote pairs. +#[cfg(not(windows))] +fn eval_posix_shell_cmd_with_quote_pairs(bytes: &[u8]) -> Vec { + let mut escaped = Vec::with_capacity(bytes.len() * 2); + let mut paired_quote_end = None; + for (index, &byte) in bytes.iter().enumerate() { + if byte == 0xff { + continue; + } + if matches!(byte, b'\'' | b'"') { + if paired_quote_end == Some(index) { + paired_quote_end = None; + escaped.push(byte); + continue; + } + if paired_quote_end.is_none() + && bytes[index + 1..].iter().position(|candidate| *candidate == byte).is_some() + { + paired_quote_end = bytes[index + 1..] + .iter() + .position(|candidate| *candidate == byte) + .map(|offset| index + offset + 1); + escaped.push(byte); + continue; + } + escaped.push(b'\\'); + escaped.push(byte); + continue; + } + let special = matches!(byte, b'#' | b'&' | b';' | b'`' | b'|' | b'*' | b'?' | b'~' | b'<' | b'>' | b'^' | b'(' | b')' | b'[' | b']' | b'{' | b'}' | b'$' | b'\\' | b'\n'); + if special { + escaped.push(b'\\'); + } + escaped.push(byte); + } + escaped +} + +#[cfg(test)] +mod windows_shell_arg_tests { + use super::eval_windows_shell_arg; + + /// Reproduces the argument-injection shape: php replaces `"`, `%` and `!` with a + /// space (ext/standard/exec.c php_escape_shell_arg breaks before the arm that + /// re-emits the byte), so none of them can survive into the quoted argument. + /// Emitting the byte as well let a quote close the quoting early. + #[test] + fn replaces_quote_percent_and_bang_instead_of_keeping_them() { + assert_eq!(eval_windows_shell_arg(br#"a"b"#), br#""a b""#); + assert_eq!(eval_windows_shell_arg(b"%PATH%"), br#"" PATH ""#); + assert_eq!(eval_windows_shell_arg(b"a!b!"), br#""a b ""#); + assert_eq!(eval_windows_shell_arg(br#"" & calc.exe & ""#), br#"" & calc.exe & ""#); + } + + /// Ordinary bytes are quoted verbatim, and an empty argument still produces a + /// quoted empty string rather than nothing. + #[test] + fn quotes_ordinary_arguments_verbatim() { + assert_eq!(eval_windows_shell_arg(b"plain"), br#""plain""#); + assert_eq!(eval_windows_shell_arg(b"with space"), br#""with space""#); + assert_eq!(eval_windows_shell_arg(b""), br#""""#); + } + + /// A trailing backslash run is padded to an even length so the closing quote is + /// not itself escaped, matching php's post-loop fixup. + #[test] + fn pads_an_odd_trailing_backslash_run() { + assert_eq!(eval_windows_shell_arg(br"a\"), br#""a\\""#); + assert_eq!(eval_windows_shell_arg(br"a\\"), br#""a\\""#); + assert_eq!(eval_windows_shell_arg(br"a\\\"), br#""a\\\\""#); + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/date.rs b/crates/elephc-magician/src/interpreter/builtins/time/date.rs index 7adcce14de..83483010ff 100644 --- a/crates/elephc-magician/src/interpreter/builtins/time/date.rs +++ b/crates/elephc-magician/src/interpreter/builtins/time/date.rs @@ -6,9 +6,11 @@ //! //! Key details: //! - `gmdate` calls this file for shared formatting and UTC/local timestamp conversion. +//! - Unix libc and the Windows 64-bit CRT are hidden behind the same timestamp helpers. -use std::os::unix::ffi::OsStrExt; use std::sync::Mutex; +#[cfg(unix)] +use std::os::unix::ffi::OsStrExt; use super::super::*; use super::*; @@ -25,11 +27,31 @@ eval_builtin! { static EVAL_TZ_MUTEX: Mutex<()> = Mutex::new(()); +#[cfg(unix)] unsafe extern "C" { /// Re-reads libc's process-global timezone environment. fn tzset(); } +#[cfg(windows)] +unsafe extern "C" { + /// Converts a 64-bit Unix timestamp into local broken-down time through the Windows CRT. + #[link_name = "_localtime64_s"] + fn windows_localtime64_s(output: *mut libc::tm, timestamp: *const i64) -> libc::c_int; + + /// Converts a 64-bit Unix timestamp into UTC broken-down time through the Windows CRT. + #[link_name = "_gmtime64_s"] + fn windows_gmtime64_s(output: *mut libc::tm, timestamp: *const i64) -> libc::c_int; + + /// Sets or removes one Windows CRT environment variable. + #[link_name = "_putenv_s"] + fn windows_putenv_s(name: *const libc::c_char, value: *const libc::c_char) -> libc::c_int; + + /// Re-reads the Windows CRT process-global timezone environment. + #[link_name = "_tzset"] + fn windows_tzset(); +} + /// Evaluates PHP `date($format, $timestamp = time())` for the eval subset. pub(in crate::interpreter) fn eval_builtin_date( args: &[EvalExpr], @@ -95,6 +117,8 @@ pub(in crate::interpreter) fn eval_context_localtime( /// Converts one Unix timestamp to process-local broken-down time through libc. pub(in crate::interpreter) fn eval_localtime(timestamp: i64) -> Result { + #[cfg(unix)] + { let raw: libc::time_t = timestamp.try_into().map_err(|_| EvalStatus::RuntimeFatal)?; let mut tm = MaybeUninit::::uninit(); let result = unsafe { libc::localtime_r(&raw, tm.as_mut_ptr()) }; @@ -102,10 +126,22 @@ pub(in crate::interpreter) fn eval_localtime(timestamp: i64) -> Result::uninit(); + let status = unsafe { windows_localtime64_s(tm.as_mut_ptr(), ×tamp) }; + if status != 0 { + return Err(EvalStatus::RuntimeFatal); + } + Ok(unsafe { tm.assume_init() }) + } } /// Converts one Unix timestamp to UTC broken-down time through libc. pub(in crate::interpreter) fn eval_gmtime(timestamp: i64) -> Result { + #[cfg(unix)] + { let raw: libc::time_t = timestamp.try_into().map_err(|_| EvalStatus::RuntimeFatal)?; let mut tm = MaybeUninit::::uninit(); let result = unsafe { libc::gmtime_r(&raw, tm.as_mut_ptr()) }; @@ -113,6 +149,16 @@ pub(in crate::interpreter) fn eval_gmtime(timestamp: i64) -> Result::uninit(); + let status = unsafe { windows_gmtime64_s(tm.as_mut_ptr(), ×tamp) }; + if status != 0 { + return Err(EvalStatus::RuntimeFatal); + } + Ok(unsafe { tm.assume_init() }) + } } /// Runs one libc timezone-sensitive operation under the eval context timezone. @@ -123,9 +169,16 @@ pub(in crate::interpreter) fn eval_with_timezone( let _guard = EVAL_TZ_MUTEX .lock() .map_err(|_| EvalStatus::RuntimeFatal)?; + #[cfg(unix)] let previous = std::env::var_os("TZ") .map(|value| CString::new(value.as_bytes()).map_err(|_| EvalStatus::RuntimeFatal)) .transpose()?; + #[cfg(windows)] + let previous = std::env::var_os("TZ") + .map(|value| { + CString::new(value.to_string_lossy().as_bytes()).map_err(|_| EvalStatus::RuntimeFatal) + }) + .transpose()?; eval_apply_process_timezone(timezone)?; let result = operation(); eval_restore_process_timezone(previous.as_ref())?; @@ -134,28 +187,36 @@ pub(in crate::interpreter) fn eval_with_timezone( /// Applies one timezone identifier to libc's process-global timezone state. fn eval_apply_process_timezone(timezone: &str) -> Result<(), EvalStatus> { - let key = CString::new("TZ").map_err(|_| EvalStatus::RuntimeFatal)?; - let value = CString::new(timezone).map_err(|_| EvalStatus::RuntimeFatal)?; - let status = unsafe { libc::setenv(key.as_ptr(), value.as_ptr(), 1) }; - if status != 0 { - return Err(EvalStatus::RuntimeFatal); - } - unsafe { tzset() }; - Ok(()) + let timezone = CString::new(timezone).map_err(|_| EvalStatus::RuntimeFatal)?; + eval_set_process_timezone(Some(timezone.as_c_str())) } /// Restores the process timezone that was active before an eval-local conversion. fn eval_restore_process_timezone(previous: Option<&CString>) -> Result<(), EvalStatus> { + eval_set_process_timezone(previous.map(|value| value.as_c_str())) +} + +/// Updates the platform CRT timezone environment and refreshes its cached timezone state. +fn eval_set_process_timezone(timezone: Option<&CStr>) -> Result<(), EvalStatus> { let key = CString::new("TZ").map_err(|_| EvalStatus::RuntimeFatal)?; - let status = if let Some(value) = previous { - unsafe { libc::setenv(key.as_ptr(), value.as_ptr(), 1) } - } else { - unsafe { libc::unsetenv(key.as_ptr()) } + let empty = CString::new("").map_err(|_| EvalStatus::RuntimeFatal)?; + let value = timezone.unwrap_or(empty.as_c_str()); + #[cfg(unix)] + let status = match timezone { + Some(_) => unsafe { libc::setenv(key.as_ptr(), value.as_ptr(), 1) }, + None => unsafe { libc::unsetenv(key.as_ptr()) }, }; + #[cfg(windows)] + let status = unsafe { windows_putenv_s(key.as_ptr(), value.as_ptr()) }; if status != 0 { return Err(EvalStatus::RuntimeFatal); } + #[cfg(unix)] unsafe { tzset() }; + #[cfg(windows)] + unsafe { + windows_tzset(); + } Ok(()) } diff --git a/crates/elephc-magician/src/interpreter/builtins/time/hrtime.rs b/crates/elephc-magician/src/interpreter/builtins/time/hrtime.rs index 87f4747462..f2d3c58338 100644 --- a/crates/elephc-magician/src/interpreter/builtins/time/hrtime.rs +++ b/crates/elephc-magician/src/interpreter/builtins/time/hrtime.rs @@ -5,10 +5,13 @@ //! - `crate::interpreter::builtins::time` direct and by-value dispatch. //! //! Key details: -//! - Monotonic time is returned as nanoseconds or `[seconds, nanoseconds]`. +//! - Rust's platform monotonic clock is returned as nanoseconds or +//! `[seconds, nanoseconds]` without depending on target-specific libc APIs. use super::super::super::*; use super::super::*; +use std::sync::OnceLock; +use std::time::Instant; use super::super::spec::EvalBuiltinDefaultValue; @@ -20,6 +23,8 @@ eval_builtin! { values: Time, } +static EVAL_MONOTONIC_ORIGIN: OnceLock = OnceLock::new(); + /// Evaluates PHP `hrtime($as_number = false)`. pub(in crate::interpreter) fn eval_builtin_hrtime( args: &[EvalExpr], @@ -61,11 +66,10 @@ pub(in crate::interpreter) fn eval_hrtime_result( /// Reads the monotonic clock in whole seconds and nanoseconds. fn eval_monotonic_time() -> Result<(i64, i64), EvalStatus> { - let mut timespec = MaybeUninit::::uninit(); - let status = unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, timespec.as_mut_ptr()) }; - if status != 0 { - return Err(EvalStatus::RuntimeFatal); - } - let timespec = unsafe { timespec.assume_init() }; - Ok((timespec.tv_sec, timespec.tv_nsec)) + let elapsed = EVAL_MONOTONIC_ORIGIN.get_or_init(Instant::now).elapsed(); + let seconds = i64::try_from(elapsed.as_secs()) + .map_err(|_| EvalStatus::RuntimeFatal)? + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + Ok((seconds, i64::from(elapsed.subsec_nanos()))) } diff --git a/crates/elephc-magician/src/interpreter/builtins/time/microtime.rs b/crates/elephc-magician/src/interpreter/builtins/time/microtime.rs index 06d13a50a9..6cac605f45 100644 --- a/crates/elephc-magician/src/interpreter/builtins/time/microtime.rs +++ b/crates/elephc-magician/src/interpreter/builtins/time/microtime.rs @@ -27,10 +27,14 @@ pub(in crate::interpreter) fn eval_builtin_microtime( values: &mut impl RuntimeValueOps, ) -> Result { match args { - [] => eval_microtime_result(values), + [] => eval_microtime_string_result(values), [as_float] => { - let _ = eval_expr(as_float, context, scope, values)?; - eval_microtime_result(values) + let flag = eval_expr(as_float, context, scope, values)?; + if values.truthy(flag)? { + eval_microtime_result(values) + } else { + eval_microtime_string_result(values) + } } _ => Err(EvalStatus::RuntimeFatal), } @@ -47,3 +51,19 @@ pub(in crate::interpreter) fn eval_microtime_result( let micros = f64::from(timestamp.subsec_micros()) / 1_000_000.0; values.float(seconds + micros) } + +/// Returns `microtime()`'s default string form: the sub-second fraction, a space, +/// then the whole seconds. +/// +/// php formats it as `"%.8F %ld"` over `tv_usec / 1e6` and `tv_sec` +/// (ext/standard/microtime.c `_php_math_microtime`), producing values such as +/// `"0.59125600 1784994272"`. Only a truthy argument selects the float form. +pub(in crate::interpreter) fn eval_microtime_string_result( + values: &mut impl RuntimeValueOps, +) -> Result { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_err(|_| EvalStatus::RuntimeFatal)?; + let fraction = f64::from(timestamp.subsec_micros()) / 1_000_000.0; + values.string(&format!("{:.8} {}", fraction, timestamp.as_secs())) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/mktime.rs b/crates/elephc-magician/src/interpreter/builtins/time/mktime.rs index c99473ac0a..57925f21a0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/time/mktime.rs +++ b/crates/elephc-magician/src/interpreter/builtins/time/mktime.rs @@ -6,6 +6,7 @@ //! //! Key details: //! - `gmmktime` and `strtotime` reuse the timestamp conversion helpers from this file. +//! - Windows uses the CRT's explicit 64-bit timestamp entry points. use super::super::*; use super::*; @@ -18,6 +19,17 @@ eval_builtin! { values: Time, } +#[cfg(windows)] +unsafe extern "C" { + /// Converts local broken-down time into a 64-bit Unix timestamp through the Windows CRT. + #[link_name = "_mktime64"] + fn windows_mktime64(time: *mut libc::tm) -> i64; + + /// Converts UTC broken-down time into a 64-bit Unix timestamp through the Windows CRT. + #[link_name = "_mkgmtime64"] + fn windows_mkgmtime64(time: *mut libc::tm) -> i64; +} + /// Evaluates PHP `mktime(hour, minute, second, month, day, year)`. pub(in crate::interpreter) fn eval_builtin_mktime( args: &[EvalExpr], @@ -110,7 +122,10 @@ pub(in crate::interpreter) fn eval_mktime_timestamp( tm.tm_mday = day; tm.tm_year = year - 1900; tm.tm_isdst = -1; + #[cfg(unix)] let timestamp = unsafe { libc::mktime(&mut tm) }; + #[cfg(windows)] + let timestamp = unsafe { windows_mktime64(&mut tm) }; i64::try_from(timestamp).map_err(|_| EvalStatus::RuntimeFatal) } @@ -133,7 +148,10 @@ pub(in crate::interpreter) fn eval_gmmktime_timestamp( tm.tm_mday = args.4; tm.tm_year = args.5 - 1900; tm.tm_isdst = 0; + #[cfg(unix)] let timestamp = unsafe { libc::timegm(&mut tm) }; + #[cfg(windows)] + let timestamp = unsafe { windows_mkgmtime64(&mut tm) }; i64::try_from(timestamp).map_err(|_| EvalStatus::RuntimeFatal) } diff --git a/crates/elephc-magician/src/interpreter/builtins/time/mod.rs b/crates/elephc-magician/src/interpreter/builtins/time/mod.rs index e1a34f7cae..c14da4f91b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/time/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/time/mod.rs @@ -162,7 +162,17 @@ pub(in crate::interpreter) fn eval_time_values_result( _ => Err(EvalStatus::RuntimeFatal), }, "microtime" => match evaluated_args { - [] | [_] => eval_microtime_result(values), + [] => eval_microtime_string_result(values), + // Only a truthy as_float selects the float form; php otherwise returns + // the " " string. This arm is the named-argument + // and call_user_func path, and it ignored the flag entirely. + [as_float] => { + if values.truthy(*as_float)? { + eval_microtime_result(values) + } else { + eval_microtime_string_result(values) + } + } _ => Err(EvalStatus::RuntimeFatal), }, "sleep" => { diff --git a/crates/elephc-magician/src/interpreter/constant_eval.rs b/crates/elephc-magician/src/interpreter/constant_eval.rs index 47bf66993c..788c0e99a5 100644 --- a/crates/elephc-magician/src/interpreter/constant_eval.rs +++ b/crates/elephc-magician/src/interpreter/constant_eval.rs @@ -147,22 +147,70 @@ pub(in crate::interpreter) fn eval_predefined_constant_value( "INF" => Some(EvalPredefinedConstant::Float(f64::INFINITY)), "NAN" => Some(EvalPredefinedConstant::Float(f64::NAN)), "PHP_INT_MAX" => Some(EvalPredefinedConstant::Int(i64::MAX)), - "PHP_EOL" => Some(EvalPredefinedConstant::String("\n")), + "PHP_EOL" => Some(EvalPredefinedConstant::String(eval_php_eol())), "PHP_OS" => Some(EvalPredefinedConstant::String(eval_php_os_name())), - "DIRECTORY_SEPARATOR" => Some(EvalPredefinedConstant::String("/")), + "PHP_OS_FAMILY" => Some(EvalPredefinedConstant::String(eval_php_os_family())), + "DIRECTORY_SEPARATOR" => Some(EvalPredefinedConstant::String(eval_directory_separator())), + "PATH_SEPARATOR" => Some(EvalPredefinedConstant::String(eval_path_separator())), _ => None, } } -/// Returns the PHP OS constant for the host platform running the eval bridge. +/// Returns `PHP_OS` for the platform this interpreter was built for. +/// +/// The interpreter is cross-compiled with the program, so `cfg!(target_os)` names +/// the target rather than the build host. The Windows arm was missing, which made +/// `eval('return PHP_OS;')` answer "Linux" inside a Windows binary whose compiled +/// `PHP_OS` said "WINNT" — the same program disagreeing with itself. fn eval_php_os_name() -> &'static str { - if cfg!(target_os = "macos") { + if cfg!(target_os = "windows") { + "WINNT" + } else if cfg!(target_os = "macos") { "Darwin" } else { "Linux" } } +/// Returns `PHP_OS_FAMILY` for the platform this interpreter was built for. +fn eval_php_os_family() -> &'static str { + if cfg!(target_os = "windows") { + "Windows" + } else if cfg!(target_os = "macos") { + "Darwin" + } else { + "Linux" + } +} + +/// Returns `PHP_EOL`: `"\r\n"` on Windows (main/php.h), `"\n"` elsewhere. +fn eval_php_eol() -> &'static str { + if cfg!(target_os = "windows") { + "\r\n" + } else { + "\n" + } +} + +/// Returns `DIRECTORY_SEPARATOR`: `DEFAULT_SLASH` is a backslash on Windows +/// (Zend/zend_virtual_cwd.h) and a forward slash everywhere else. +fn eval_directory_separator() -> &'static str { + if cfg!(target_os = "windows") { + "\\" + } else { + "/" + } +} + +/// Returns `PATH_SEPARATOR`: `";"` on Windows, `":"` elsewhere. +fn eval_path_separator() -> &'static str { + if cfg!(target_os = "windows") { + ";" + } else { + ":" + } +} + /// Resolves one eval magic constant against fragment and dynamic-call metadata. pub(super) fn eval_magic_const( magic: &EvalMagicConst, @@ -190,3 +238,53 @@ pub(super) fn eval_magic_const( EvalMagicConst::Trait => values.string(context.current_magic_trait().unwrap_or("")), } } + +#[cfg(test)] +mod platform_constant_tests { + use super::{ + eval_directory_separator, eval_path_separator, eval_php_eol, eval_php_os_family, + eval_php_os_name, + }; + + /// Verifies the interpreter reports the constants of the platform it was built + /// for, so an eval fragment cannot contradict the compiled constants in the same + /// binary. The Windows arms were missing entirely, which made a Windows program + /// answer PHP_OS "Linux" from inside eval() while its compiled PHP_OS said + /// "WINNT". + #[test] + fn reports_the_target_platform_constants() { + if cfg!(target_os = "windows") { + assert_eq!(eval_php_os_name(), "WINNT"); + assert_eq!(eval_php_os_family(), "Windows"); + assert_eq!(eval_php_eol(), "\r\n"); + assert_eq!(eval_directory_separator(), "\\"); + assert_eq!(eval_path_separator(), ";"); + } else if cfg!(target_os = "macos") { + assert_eq!(eval_php_os_name(), "Darwin"); + assert_eq!(eval_php_os_family(), "Darwin"); + assert_eq!(eval_php_eol(), "\n"); + assert_eq!(eval_directory_separator(), "/"); + assert_eq!(eval_path_separator(), ":"); + } else { + assert_eq!(eval_php_os_name(), "Linux"); + assert_eq!(eval_php_os_family(), "Linux"); + assert_eq!(eval_php_eol(), "\n"); + assert_eq!(eval_directory_separator(), "/"); + assert_eq!(eval_path_separator(), ":"); + } + } + + /// Verifies the POSIX separators never carry a Windows shape and vice versa, so a + /// future edit cannot make one constant disagree with the others. + #[test] + fn separators_are_internally_consistent() { + let sep = eval_directory_separator(); + let path_sep = eval_path_separator(); + let eol = eval_php_eol(); + if sep == "\\" { + assert_eq!((path_sep, eol), (";", "\r\n"), "windows shape must be complete"); + } else { + assert_eq!((sep, path_sep, eol), ("/", ":", "\n"), "posix shape must be complete"); + } + } +} diff --git a/crates/elephc-magician/src/interpreter/constants.rs b/crates/elephc-magician/src/interpreter/constants.rs index 281e2a56ff..884d277f0d 100644 --- a/crates/elephc-magician/src/interpreter/constants.rs +++ b/crates/elephc-magician/src/interpreter/constants.rs @@ -59,9 +59,15 @@ pub(super) const EVAL_STREAM_WRAPPERS: &[&str] = &[ ]; /// Built-in stream transports reported by eval `stream_get_transports()`. +#[cfg(not(windows))] pub(super) const EVAL_STREAM_TRANSPORTS: &[&str] = &[ - "tcp", "udp", "unix", "udg", "tls", "ssl", "sslv2", "sslv3", "tlsv1.0", "tlsv1.1", "tlsv1.2", - "tlsv1.3", + "tcp", "udp", "unix", "udg", "tls", "ssl", "tlsv1.0", "tlsv1.1", "tlsv1.2", "tlsv1.3", +]; + +/// Built-in stream transports reported by eval `stream_get_transports()` on Windows. +#[cfg(windows)] +pub(super) const EVAL_STREAM_TRANSPORTS: &[&str] = &[ + "tcp", "udp", "tls", "ssl", "tlsv1.0", "tlsv1.1", "tlsv1.2", "tlsv1.3", ]; /// Monotonic salt mixed into eval `rand()`/`mt_rand()` and array key sampling. diff --git a/crates/elephc-magician/src/interpreter/expressions/calls.rs b/crates/elephc-magician/src/interpreter/expressions/calls.rs index a44858ef0b..df7f78a9fe 100644 --- a/crates/elephc-magician/src/interpreter/expressions/calls.rs +++ b/crates/elephc-magician/src/interpreter/expressions/calls.rs @@ -56,6 +56,9 @@ pub(in crate::interpreter) fn eval_call( if name == "flock" { return eval_builtin_flock(args, context, scope, values); } + if name == "proc_open" { + return eval_builtin_proc_open_call(args, context, scope, values); + } if name == "preg_match" { return eval_builtin_preg_match_call(args, context, scope, values); } diff --git a/crates/elephc-magician/src/interpreter/libc_shims.rs b/crates/elephc-magician/src/interpreter/libc_shims.rs index 1f6898483c..3aa9d5c261 100644 --- a/crates/elephc-magician/src/interpreter/libc_shims.rs +++ b/crates/elephc-magician/src/interpreter/libc_shims.rs @@ -1,45 +1,467 @@ //! Purpose: -//! Declares libc routines used by eval builtins that mirror PHP process and network helpers. +//! Provides portable OS shims used by eval builtins that mirror PHP network and system helpers. //! //! Called from: //! - `crate::interpreter::builtins::network_env` //! - `crate::interpreter::builtins::filesystem` //! //! Key details: -//! - These bindings are unsafe FFI declarations only; call sites own pointer validation and -//! PHP-compatible fallback behavior. +//! - Unix delegates to libc while Windows initializes Winsock and uses Win32 system APIs. +//! - Process-global resolver records are copied before returning to callers. +use std::ffi::{CStr, c_char, c_int}; +#[cfg(unix)] +use std::ffi::c_void; + +/// Owns the five fields exposed through PHP's `php_uname()` modes. +pub(super) struct EvalUnameFields { + pub(super) sysname: Vec, + pub(super) nodename: Vec, + pub(super) release: Vec, + pub(super) version: Vec, + pub(super) machine: Vec, +} + +/// Copies one NUL-terminated C character array into an owned byte vector. +#[cfg(unix)] +fn nul_terminated_bytes(field: &[c_char]) -> Vec { + let length = field + .iter() + .position(|byte| *byte == 0) + .unwrap_or(field.len()); + field[..length].iter().map(|byte| *byte as u8).collect() +} + +/// Returns the current hostname as raw PHP string bytes. +#[cfg(unix)] +pub(super) fn eval_os_hostname() -> Option> { + let mut buffer = [0 as c_char; 256]; + let status = unsafe { + // libc writes at most buffer.len() bytes into this stack buffer. + libc::gethostname(buffer.as_mut_ptr(), buffer.len()) + }; + (status == 0).then(|| nul_terminated_bytes(&buffer)) +} + +/// Returns the current hostname as raw PHP string bytes. +#[cfg(windows)] +pub(super) fn eval_os_hostname() -> Option> { + windows::hostname() +} + +/// Reverse-resolves an IPv4 address and copies the canonical host name. +#[cfg(unix)] +pub(super) fn eval_reverse_ipv4_name(octets: [u8; 4]) -> Option> { + let host = unsafe { + // libc reads the stack-owned IPv4 octets and returns process-global storage. + libc_gethostbyaddr( + octets.as_ptr().cast::(), + octets.len() as libc::socklen_t, + libc::AF_INET, + ) + }; + unsafe { copy_host_name(host) } +} + +/// Reverse-resolves an IPv4 address and copies the canonical host name. +#[cfg(windows)] +pub(super) fn eval_reverse_ipv4_name(octets: [u8; 4]) -> Option> { + windows::reverse_ipv4_name(octets) +} + +/// Looks up an IP protocol number by canonical name or alias. +#[cfg(unix)] +pub(super) fn eval_protocol_number(name: &CStr) -> Option { + let entry = unsafe { libc_getprotobyname(name.as_ptr()) }; + (!entry.is_null()).then(|| unsafe { (*entry).p_proto }) +} + +/// Looks up an IP protocol number by canonical name or alias. +#[cfg(windows)] +pub(super) fn eval_protocol_number(name: &CStr) -> Option { + windows::protocol_number(name) +} + +/// Looks up and copies an IP protocol's canonical name. +#[cfg(unix)] +pub(super) fn eval_protocol_name(number: i32) -> Option> { + let entry = unsafe { libc_getprotobynumber(number) }; + unsafe { copy_c_name((!entry.is_null()).then(|| (*entry).p_name)) } +} + +/// Looks up and copies an IP protocol's canonical name. +#[cfg(windows)] +pub(super) fn eval_protocol_name(number: i32) -> Option> { + windows::protocol_name(number) +} + +/// Looks up an internet service port by service name and protocol. +#[cfg(unix)] +pub(super) fn eval_service_port(service: &CStr, protocol: &CStr) -> Option { + let entry = unsafe { libc_getservbyname(service.as_ptr(), protocol.as_ptr()) }; + (!entry.is_null()).then(|| unsafe { u16::from_be((*entry).s_port as u16) }) +} + +/// Looks up an internet service port by service name and protocol. +#[cfg(windows)] +pub(super) fn eval_service_port(service: &CStr, protocol: &CStr) -> Option { + windows::service_port(service, protocol) +} + +/// Looks up and copies an internet service's canonical name. +#[cfg(unix)] +pub(super) fn eval_service_name(port: u16, protocol: &CStr) -> Option> { + let entry = unsafe { libc_getservbyport(port.to_be() as c_int, protocol.as_ptr()) }; + unsafe { copy_c_name((!entry.is_null()).then(|| (*entry).s_name)) } +} + +/// Looks up and copies an internet service's canonical name. +#[cfg(windows)] +pub(super) fn eval_service_name(port: u16, protocol: &CStr) -> Option> { + windows::service_name(port, protocol) +} + +/// Reads the operating-system identity fields used by `php_uname()`. +#[cfg(unix)] +pub(super) fn eval_os_uname() -> Option { + let mut utsname = std::mem::MaybeUninit::::zeroed(); + let status = unsafe { + // libc initializes the entire stack-owned structure on success. + libc::uname(utsname.as_mut_ptr()) + }; + if status != 0 { + return None; + } + let utsname = unsafe { utsname.assume_init() }; + Some(EvalUnameFields { + sysname: nul_terminated_bytes(&utsname.sysname), + nodename: nul_terminated_bytes(&utsname.nodename), + release: nul_terminated_bytes(&utsname.release), + version: nul_terminated_bytes(&utsname.version), + machine: nul_terminated_bytes(&utsname.machine), + }) +} + +/// Reads the operating-system identity fields used by `php_uname()`. +#[cfg(windows)] +pub(super) fn eval_os_uname() -> Option { + windows::uname() +} + +/// Sets the process file-creation mask and returns the previous mask. +#[cfg(unix)] +pub(super) fn eval_os_umask(mask: u32) -> u32 { + unsafe { umask(mask) } +} + +/// Sets the process file-creation mask and returns the previous mask. +#[cfg(windows)] +pub(super) fn eval_os_umask(mask: u32) -> u32 { + windows::umask(mask) +} + +/// Copies a nullable C string pointer into owned storage. +#[cfg(unix)] +unsafe fn copy_c_name(name: Option<*mut c_char>) -> Option> { + let name = name?; + if name.is_null() { + return None; + } + Some(unsafe { CStr::from_ptr(name) }.to_bytes().to_vec()) +} + +/// Copies a libc host entry's canonical name into owned storage. +#[cfg(unix)] +unsafe fn copy_host_name(host: *mut libc::hostent) -> Option> { + if host.is_null() { + return None; + } + unsafe { copy_c_name(Some((*host).h_name)) } +} + +#[cfg(unix)] unsafe extern "C" { /// Reverse-resolves one socket address through libc's `gethostbyaddr`. #[link_name = "gethostbyaddr"] - pub(super) fn libc_gethostbyaddr( - addr: *const libc::c_void, + fn libc_gethostbyaddr( + addr: *const c_void, len: libc::socklen_t, - type_: libc::c_int, + type_: c_int, ) -> *mut libc::hostent; /// Looks up one IP protocol entry by protocol name or alias. #[link_name = "getprotobyname"] - pub(super) fn libc_getprotobyname(name: *const libc::c_char) -> *mut libc::protoent; + fn libc_getprotobyname(name: *const c_char) -> *mut libc::protoent; /// Looks up one IP protocol entry by protocol number. #[link_name = "getprotobynumber"] - pub(super) fn libc_getprotobynumber(proto: libc::c_int) -> *mut libc::protoent; + fn libc_getprotobynumber(proto: c_int) -> *mut libc::protoent; /// Looks up one internet service entry by service name and protocol. #[link_name = "getservbyname"] - pub(super) fn libc_getservbyname( - name: *const libc::c_char, - proto: *const libc::c_char, - ) -> *mut libc::servent; + fn libc_getservbyname(name: *const c_char, proto: *const c_char) -> *mut libc::servent; /// Looks up one internet service entry by port and protocol. #[link_name = "getservbyport"] - pub(super) fn libc_getservbyport( - port: libc::c_int, - proto: *const libc::c_char, - ) -> *mut libc::servent; + fn libc_getservbyport(port: c_int, proto: *const c_char) -> *mut libc::servent; /// Sets the process file-creation mask and returns the previous mask. pub(super) fn umask(mask: u32) -> u32; } + +#[cfg(windows)] +mod windows { + use super::*; + use std::mem::MaybeUninit; + use std::sync::OnceLock; + + const AF_INET: c_int = 2; + const WINSOCK_VERSION_2_2: u16 = 0x0202; + + #[repr(C)] + struct WsaData { + version: u16, + high_version: u16, + description: [u8; 257], + system_status: [u8; 129], + max_sockets: u16, + max_udp_datagram: u16, + vendor_info: *mut c_char, + } + + #[repr(C)] + struct HostEnt { + name: *mut c_char, + aliases: *mut *mut c_char, + address_type: i16, + address_length: i16, + address_list: *mut *mut c_char, + } + + #[repr(C)] + struct ProtoEnt { + name: *mut c_char, + aliases: *mut *mut c_char, + protocol: i16, + } + + #[repr(C)] + struct OsVersionInfo { + size: u32, + major: u32, + minor: u32, + build: u32, + platform_id: u32, + service_pack: [u16; 128], + service_pack_major: u16, + service_pack_minor: u16, + suite_mask: u16, + product_type: u8, + reserved: u8, + } + + static WINSOCK_READY: OnceLock = OnceLock::new(); + + /// Initializes Winsock 2.2 once for the process before database lookups. + fn ensure_winsock() -> bool { + *WINSOCK_READY.get_or_init(|| { + let mut data = MaybeUninit::::zeroed(); + unsafe { WSAStartup(WINSOCK_VERSION_2_2, data.as_mut_ptr()) == 0 } + }) + } + + /// Copies a nullable Winsock-owned C string into owned storage. + unsafe fn copy_name(name: *mut c_char) -> Option> { + if name.is_null() { + return None; + } + Some(unsafe { CStr::from_ptr(name) }.to_bytes().to_vec()) + } + + /// Returns the current host name through Winsock. + pub(super) fn hostname() -> Option> { + if !ensure_winsock() { + return None; + } + let mut buffer = [0 as c_char; 256]; + if unsafe { gethostname(buffer.as_mut_ptr(), buffer.len() as c_int) } != 0 { + return None; + } + unsafe { copy_name(buffer.as_mut_ptr()) } + } + + /// Reverse-resolves one IPv4 address through Winsock. + pub(super) fn reverse_ipv4_name(octets: [u8; 4]) -> Option> { + if !ensure_winsock() { + return None; + } + let host = unsafe { + gethostbyaddr( + octets.as_ptr().cast::(), + octets.len() as c_int, + AF_INET, + ) + }; + if host.is_null() { + return None; + } + unsafe { copy_name((*host).name) } + } + + /// Looks up a protocol number through the Winsock protocol database. + pub(super) fn protocol_number(name: &CStr) -> Option { + if !ensure_winsock() { + return None; + } + let entry = unsafe { getprotobyname(name.as_ptr()) }; + (!entry.is_null()).then(|| unsafe { i32::from((*entry).protocol) }) + } + + /// Looks up a protocol name through the Winsock protocol database. + pub(super) fn protocol_name(number: i32) -> Option> { + if !ensure_winsock() { + return None; + } + let entry = unsafe { getprotobynumber(number) }; + if entry.is_null() { + return None; + } + unsafe { copy_name((*entry).name) } + } + + /// Looks up a service port through the Winsock services database. + pub(super) fn service_port(service: &CStr, protocol: &CStr) -> Option { + let service = service.to_string_lossy(); + let protocol = protocol.to_string_lossy(); + services_records().find_map(|record| { + (record.protocol.eq_ignore_ascii_case(&protocol) + && record.names.iter().any(|name| name.eq_ignore_ascii_case(&service))) + .then_some(record.port) + }) + } + + /// Looks up a service name through the Winsock services database. + pub(super) fn service_name(port: u16, protocol: &CStr) -> Option> { + let protocol = protocol.to_string_lossy(); + services_records() + .find(|record| record.port == port && record.protocol.eq_ignore_ascii_case(&protocol)) + .and_then(|record| record.names.into_iter().next()) + .map(String::into_bytes) + } + + /// Owns one parsed line from Windows' system services database. + struct ServiceRecord { + names: Vec, + port: u16, + protocol: String, + } + + /// Reads and parses Windows' canonical services database. + fn services_records() -> impl Iterator { + let windows_root = std::env::var_os("SystemRoot") + .or_else(|| std::env::var_os("WINDIR")) + .unwrap_or_else(|| r"C:\Windows".into()); + let path = std::path::PathBuf::from(windows_root) + .join("System32") + .join("drivers") + .join("etc") + .join("services"); + std::fs::read_to_string(path) + .unwrap_or_default() + .lines() + .filter_map(parse_service_record) + .collect::>() + .into_iter() + } + + /// Parses one services file line, preserving its canonical name and aliases. + fn parse_service_record(line: &str) -> Option { + let line = line.split('#').next()?.trim(); + if line.is_empty() { + return None; + } + let mut fields = line.split_whitespace(); + let canonical_name = fields.next()?; + let (port, protocol) = fields.next()?.split_once('/')?; + let port = port.parse::().ok()?; + let mut names = vec![canonical_name.to_string()]; + names.extend(fields.map(str::to_string)); + Some(ServiceRecord { + names, + port, + protocol: protocol.to_string(), + }) + } + + /// Reads Windows version data with the manifest-independent `RtlGetVersion` API. + pub(super) fn uname() -> Option { + let mut version = OsVersionInfo { + size: std::mem::size_of::() as u32, + major: 0, + minor: 0, + build: 0, + platform_id: 0, + service_pack: [0; 128], + service_pack_major: 0, + service_pack_minor: 0, + suite_mask: 0, + product_type: 0, + reserved: 0, + }; + if unsafe { RtlGetVersion(&mut version) } < 0 { + return None; + } + let nodename = hostname().unwrap_or_default(); + let release = format!("{}.{}", version.major, version.minor).into_bytes(); + let service_pack_length = version + .service_pack + .iter() + .position(|unit| *unit == 0) + .unwrap_or(version.service_pack.len()); + let service_pack = String::from_utf16_lossy(&version.service_pack[..service_pack_length]); + let version_text = if service_pack.is_empty() { + format!("build {}", version.build) + } else { + format!("build {} ({service_pack})", version.build) + }; + Some(EvalUnameFields { + sysname: b"Windows NT".to_vec(), + nodename, + release, + version: version_text.into_bytes(), + machine: std::env::consts::ARCH.as_bytes().to_vec(), + }) + } + + /// Sets the process file-creation mask through the Microsoft C runtime. + pub(super) fn umask(mask: u32) -> u32 { + unsafe { c_umask(mask as c_int) as u32 } + } + + #[link(name = "ws2_32")] + unsafe extern "system" { + /// Initializes the requested Winsock API version for this process. + fn WSAStartup(version: u16, data: *mut WsaData) -> c_int; + /// Writes the local hostname into the caller-owned buffer. + fn gethostname(name: *mut c_char, length: c_int) -> c_int; + /// Reverse-resolves one network address through Winsock's resolver database. + fn gethostbyaddr(address: *const c_char, length: c_int, kind: c_int) -> *mut HostEnt; + /// Looks up one protocol database entry by name or alias. + fn getprotobyname(name: *const c_char) -> *mut ProtoEnt; + /// Looks up one protocol database entry by numeric identifier. + fn getprotobynumber(number: c_int) -> *mut ProtoEnt; + } + + #[link(name = "ntdll")] + unsafe extern "system" { + /// Reads the real Windows version independently of application manifests. + fn RtlGetVersion(version: *mut OsVersionInfo) -> i32; + } + + #[link(name = "msvcrt")] + unsafe extern "C" { + /// Sets the Microsoft C runtime file-creation mask. + #[link_name = "_umask"] + fn c_umask(mask: c_int) -> c_int; + } +} diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index 61ad04af78..79feec54f7 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -80,6 +80,7 @@ use throwables::*; use std::ffi::{CStr, CString}; use std::mem::MaybeUninit; use std::net::ToSocketAddrs; +#[cfg(unix)] use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; use std::sync::atomic::Ordering; use std::time::{SystemTime, UNIX_EPOCH}; diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_arrays_sets.rs b/crates/elephc-magician/src/interpreter/tests/builtins_arrays_sets.rs index 0686df941f..080b21b2e7 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_arrays_sets.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_arrays_sets.rs @@ -275,9 +275,16 @@ $secureCall = call_user_func("random_int", 5, 5); echo ($secureCall === 5 ? "random-call" : "bad") . ":"; $secureSpread = call_user_func_array("random_int", ["min" => 6, "max" => 6]); echo ($secureSpread === 6 ? "random-spread" : "bad") . ":"; +$bytes = random_bytes(length: 16); +echo (strlen($bytes) === 16 ? "bytes" : "bad") . ":"; +$bytesCall = call_user_func("random_bytes", 8); +echo (strlen($bytesCall) === 8 ? "bytes-call" : "bad") . ":"; +$bytesSpread = call_user_func_array("random_bytes", ["length" => 4]); +echo (strlen($bytesSpread) === 4 ? "bytes-spread" : "bad") . ":"; echo function_exists("rand"); echo function_exists("mt_rand"); -return function_exists("random_int");"#, +echo function_exists("random_int"); +return function_exists("random_bytes");"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); @@ -287,7 +294,8 @@ return function_exists("random_int");"#, assert_eq!( values.output, - "plain:range:same:swap:call:spread:random:random-call:random-spread:11" + "plain:range:same:swap:call:spread:random:random-call:random-spread:\ +bytes:bytes-call:bytes-spread:111" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_filesystem_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_filesystem_metadata.rs index 2c18e4bcf8..fbd2f1b6ad 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_filesystem_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_filesystem_metadata.rs @@ -218,7 +218,7 @@ return true;"# let _ = std::fs::remove_file(&filename); let _ = std::fs::remove_file(&link); std::fs::write(&filename, b"hello").expect("write stat fixture"); - std::os::unix::fs::symlink(&filename, &link).expect("create stat symlink"); + create_file_symlink(&filename, &link).expect("create stat symlink"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); @@ -258,7 +258,7 @@ return true;"# let _ = std::fs::remove_file(&filename); let _ = std::fs::remove_file(&link); std::fs::write(&filename, b"hello").expect("write stat array fixture"); - std::os::unix::fs::symlink(&filename, &link).expect("create stat array symlink"); + create_file_symlink(&filename, &link).expect("create stat array symlink"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); @@ -272,3 +272,15 @@ return true;"# ); assert_eq!(values.get(result), FakeValue::Bool(true)); } + +/// Creates a file symlink with the host platform's standard-library API. +fn create_file_symlink(original: &str, link: &str) -> std::io::Result<()> { + #[cfg(unix)] + { + std::os::unix::fs::symlink(original, link) + } + #[cfg(windows)] + { + std::os::windows::fs::symlink_file(original, link) + } +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_filesystem_ops.rs b/crates/elephc-magician/src/interpreter/tests/builtins_filesystem_ops.rs index fc25eb689d..7b7e840f87 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_filesystem_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_filesystem_ops.rs @@ -220,10 +220,14 @@ fn execute_program_dispatches_file_modify_builtins() { let missing = format!("elephc_magician_modify_missing_{pid}.txt"); let prefix = format!("evm{pid}_"); let call_prefix = format!("evc{pid}_"); + #[cfg(unix)] + let mode_check = format!("(fileperms(\"{filename}\") & 511) === 384"); + #[cfg(windows)] + let mode_check = format!("(fileperms(\"{filename}\") & 128) !== 0"); let source = format!( r#"file_put_contents("{filename}", "x"); echo chmod(filename: "{filename}", permissions: 384) ? "chmod" : "bad"; echo ":"; -echo (fileperms("{filename}") & 511) === 384 ? "mode" : "bad"; echo ":"; +echo {mode_check} ? "mode" : "bad"; echo ":"; echo chmod("{missing}", 384) ? "bad" : "chmod-false"; echo ":"; $tmp = tempnam(directory: ".", prefix: "{prefix}"); echo file_exists($tmp) && str_starts_with(basename($tmp), "{prefix}") ? "tempnam" : "bad"; echo ":"; @@ -273,8 +277,61 @@ return true;"# ); assert_eq!(values.get(result), FakeValue::Bool(true)); } + +/// Verifies Windows chmod metadata follows aliases and filesystem mutations without leaking. +#[cfg(windows)] +#[test] +fn execute_program_tracks_windows_chmod_metadata_by_file_identity() { + let pid = std::process::id(); + let source_file = format!("elephc_magician_ModeAlias_{pid}.txt"); + let case_alias = source_file.to_lowercase(); + let hard_link = format!("elephc_magician_mode_hard_{pid}.txt"); + let renamed = format!("elephc_magician_mode_renamed_{pid}.txt"); + let copied = format!("elephc_magician_mode_copied_{pid}.txt"); + let plain = format!("elephc_magician_mode_plain_{pid}.txt"); + let replaced = format!("elephc_magician_mode_replaced_{pid}.txt"); + let source = format!( + r#"file_put_contents("{source_file}", "source"); +chmod("{source_file}", 384); +echo (fileperms("./{case_alias}") & 511) === 384 ? "alias" : "bad"; echo ":"; +link("{source_file}", "{hard_link}"); +unlink("{source_file}"); +echo (fileperms("{hard_link}") & 511) === 384 ? "hard" : "bad"; echo ":"; +rename("{hard_link}", "{renamed}"); +echo (fileperms("{renamed}") & 511) === 384 ? "rename" : "bad"; echo ":"; +copy("{renamed}", "{copied}"); +echo (fileperms("{copied}") & 511) === 384 ? "copy" : "bad"; echo ":"; +chmod("{copied}", 420); +unlink("{copied}"); +file_put_contents("{copied}", "replacement"); +echo (fileperms("{copied}") & 511) !== 420 ? "recreate" : "bad"; echo ":"; +file_put_contents("{plain}", "plain"); +file_put_contents("{replaced}", "old"); +chmod("{replaced}", 420); +copy("{plain}", "{replaced}"); +echo (fileperms("{replaced}") & 511) !== 420 ? "replace" : "bad"; echo ":"; +unlink("{renamed}"); unlink("{copied}"); unlink("{plain}"); unlink("{replaced}"); +return true;"# + ); + let paths = [&source_file, &case_alias, &hard_link, &renamed, &copied, &plain, &replaced]; + for path in paths { + let _ = std::fs::remove_file(path); + } + let program = parse_fragment(source.as_bytes()).expect("parse Windows chmod metadata fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + for path in paths { + let _ = std::fs::remove_file(path); + } + assert_eq!(values.output, "alias:hard:rename:copy:recreate:replace:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} /// Verifies eval ownership builtins mutate local files and dispatch dynamically. #[test] +#[cfg(unix)] fn execute_program_dispatches_file_ownership_builtins() { let pid = std::process::id(); let filename = format!("elephc_magician_ownership_{pid}.txt"); @@ -316,6 +373,46 @@ return function_exists("lchgrp");"# ); assert_eq!(values.get(result), FakeValue::Bool(true)); } + +/// Verifies Windows local ownership builtins fail silently like PHP while remaining callable. +#[test] +#[cfg(windows)] +fn execute_program_dispatches_windows_file_ownership_builtins() { + let pid = std::process::id(); + let filename = format!("elephc_magician_ownership_{pid}.txt"); + let missing = format!("elephc_magician_ownership_missing_{pid}.txt"); + let source = format!( + r#"file_put_contents("{filename}", "x"); +echo chown("{filename}", 0) ? "bad" : "chown-false"; echo ":"; +echo chgrp(filename: "{filename}", group: 0) ? "bad" : "chgrp-false"; echo ":"; +echo chown("{filename}", "__elephc_eval_missing_user__") ? "bad" : "user-false"; echo ":"; +echo chgrp("{filename}", "__elephc_eval_missing_group__") ? "bad" : "group-false"; echo ":"; +echo chown("{missing}", 0) ? "bad" : "missing-false"; echo ":"; +echo call_user_func("chown", "{filename}", 0) ? "bad" : "call-false"; echo ":"; +echo call_user_func_array("chgrp", ["filename" => "{filename}", "group" => 0]) ? "bad" : "array-false"; echo ":"; +echo unlink("{filename}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("chown"); echo function_exists("chgrp"); +echo function_exists("lchown"); echo function_exists("lchgrp"); +return true;"# + ); + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&missing); + let program = parse_fragment(source.as_bytes()).expect("parse Windows eval ownership fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values) + .expect("execute Windows eval ownership fragment"); + + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&missing); + assert_eq!( + values.output, + "chown-false:chgrp-false:user-false:group-false:missing-false:call-false:array-false:cleanup:1100" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies eval `touch()` creates files, stamps mtimes, and dispatches dynamically. #[test] fn execute_program_dispatches_touch_builtin() { diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_process_pipes.rs b/crates/elephc-magician/src/interpreter/tests/builtins_process_pipes.rs index e517fad9aa..35485236d9 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_process_pipes.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_process_pipes.rs @@ -13,6 +13,7 @@ use super::support::*; /// Verifies `popen()` and `pclose()` support read/write pipes and dynamic calls. #[test] +#[cfg(unix)] fn execute_program_dispatches_process_pipe_builtins() { let pid = std::process::id(); let file = format!("elephc_magician_popen_{pid}.txt"); @@ -46,3 +47,138 @@ return true;"# ); assert_eq!(values.get(result), FakeValue::Bool(true)); } + +/// Verifies Windows `popen()` uses `cmd.exe` pipes without Unix descriptor conversion. +#[test] +#[cfg(windows)] +fn execute_program_dispatches_windows_process_pipe_builtins() { + let program = parse_fragment( + br#"$h = popen("echo|set /p=eval-popen", "r"); +echo is_resource($h) ? "open" : "bad"; echo ":"; +echo fread($h, 64) === "eval-popen" ? "read" : "bad"; echo ":"; +echo pclose($h) === 0 ? "closed" : "bad"; echo ":"; +echo function_exists("popen"); echo function_exists("pclose"); +return true;"#, + ) + .expect("parse Windows eval process pipe fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "open:read:closed:11"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval `proc_open` owns a real child process, writes the pipes output, +/// and `proc_close` returns the child's exit status. +#[test] +#[cfg(unix)] +fn execute_program_dispatches_proc_open_and_close() { + let root = std::env::temp_dir().join(format!( + "elephc_magician_proc_open_{}", + std::process::id() + )); + std::fs::create_dir_all(&root).expect("create proc_open cwd"); + let root = std::fs::canonicalize(&root).expect("canonicalize proc_open cwd"); + let output = root.join("redirected.txt"); + let source = format!( + r#"$pipes = []; +$process = proc_open('read line; printf "%s|%s|%s" "$MAGIC" "$PWD" "$line"; printf "stderr" >&2; exit 9', [0 => ["pipe", "r"], 1 => ["pipe", "w"], 2 => ["pipe", "w"]], $pipes, "{}", ["MAGIC" => "env"]); +echo is_resource($process) ? "open" : "bad"; echo ":"; +echo fwrite($pipes[0], "hello\n"); echo ":"; +echo fclose($pipes[0]) ? "closed" : "bad"; echo ":"; +echo stream_get_contents($pipes[1]); echo ":"; +echo stream_get_contents($pipes[2]); echo ":"; +echo fclose($pipes[1]) ? "outclosed" : "bad"; echo ":"; +echo fclose($pipes[2]) ? "errclosed" : "bad"; echo ":"; +echo proc_close($process); echo ":"; +$redirected = []; +$second = proc_open('printf "file-out"; printf "%s" "-err" >&2; exit 7', [1 => ["file", "{}", "w"], 2 => 1], $redirected); +echo count($redirected); echo ":"; +echo proc_close($second); echo ":"; +echo file_get_contents("{}"); echo ":"; +$direct_pipes = []; +$direct = proc_open('/usr/bin/printf bypass', [1 => ["pipe", "w"]], $direct_pipes, null, null, ["bypass_shell" => true]); +echo stream_get_contents($direct_pipes[1]); echo ":"; +echo proc_close($direct); echo ":"; +echo function_exists("proc_open"); echo function_exists("proc_close"); +return true;"#, + root.to_string_lossy(), + output.to_string_lossy(), + output.to_string_lossy(), + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval proc_open fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_dir_all(&root); + assert_eq!( + values.output, + format!( + "open:6:closed:env|{}|hello:stderr:outclosed:errclosed:9:0:7:file-out-err:bypass:0:11", + root.to_string_lossy() + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval process status is non-consuming and the new process builtins are registered. +#[test] +#[cfg(unix)] +fn execute_program_dispatches_proc_status_and_terminate_builtins() { + let program = parse_fragment( + br#"$pipes = []; +$process = proc_open('sleep 1', [1 => ["pipe", "w"]], $pipes); +$status = proc_get_status($process); +echo $status["running"] ? "running" : "stopped"; echo ":"; +echo $status["cached"] ? "cached" : "fresh"; echo ":"; +echo is_int($status["pid"]) ? "pid" : "bad"; echo ":"; +echo call_user_func("proc_get_status", $process)["command"] === "sleep 1" ? "command" : "bad"; echo ":"; +echo proc_close($process) === 0 ? "closed" : "bad"; echo ":"; +echo proc_terminate($process) ? "bad" : "notfound"; echo ":"; +return function_exists("proc_get_status") && function_exists("proc_terminate");"#, + ) + .expect("parse eval proc status fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "running:fresh:pid:command:closed:notfound:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies Windows eval `proc_open` materializes all three pipe directions and +/// preserves the child exit code under `cmd.exe`. +#[test] +#[cfg(windows)] +fn execute_program_dispatches_windows_proc_open_descriptors() { + let program = parse_fragment( + br#"$pipes = []; +$process = proc_open("more >NUL&&2 ["pipe", "r"], 1 => ["pipe", "w"], 2 => ["pipe", "w"]], $pipes, null, ["MAGIC" => "env"]); +echo is_resource($process) ? "open" : "bad"; echo ":"; +echo fwrite($pipes[0], "hello\r\n"); echo ":"; +echo fclose($pipes[0]) ? "closed" : "bad"; echo ":"; +echo stream_get_contents($pipes[1]); echo ":"; +echo stream_get_contents($pipes[2]); echo ":"; +echo fclose($pipes[1]) ? "outclosed" : "bad"; echo ":"; +echo fclose($pipes[2]) ? "errclosed" : "bad"; echo ":"; +echo proc_close($process); echo ":"; +echo function_exists("proc_open"); echo function_exists("proc_close"); +return true;"#, + ) + .expect("parse Windows eval proc_open fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "open:7:closed:env:stderr:outclosed:errclosed:9:11" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_strings_binary.rs b/crates/elephc-magician/src/interpreter/tests/builtins_strings_binary.rs index d425a64573..313cdb53fc 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_strings_binary.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_strings_binary.rs @@ -250,6 +250,26 @@ return function_exists("addslashes") && function_exists("stripslashes");"#, ); assert_eq!(values.get(result), FakeValue::Bool(true)); } + +/// Verifies eval shell escaping is registry-visible through direct and callable dispatch. +#[test] +#[cfg(not(windows))] +fn execute_program_dispatches_posix_shell_escape_builtins() { + let program = parse_fragment( + br#"echo escapeshellarg("a'b"); echo ":"; +echo escapeshellcmd("a&b"); echo ":"; +echo call_user_func("escapeshellcmd", "x|y"); echo ":"; +return function_exists("escapeshellarg") && function_exists("escapeshellcmd");"#, + ) + .expect("parse eval shell escaping fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "'a'\\''b':a\\&b:x\\|y:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} /// Verifies eval `base64_encode()` dispatches through direct, named, and callable paths. #[test] fn execute_program_dispatches_base64_encode_builtin() { diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_system_network.rs b/crates/elephc-magician/src/interpreter/tests/builtins_system_network.rs index f4fd060253..d52686662a 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_system_network.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_system_network.rs @@ -31,10 +31,14 @@ return function_exists("sys_get_temp_dir");"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + // The temporary directory is whatever php would resolve here, not the "/tmp" + // literal the interpreter used to return: TMPDIR wins, so on macOS this is a + // per-user /var/folders//T path. + let temp_dir = super::super::builtins::filesystem::sys_get_temp_dir::eval_temp_dir(); assert_eq!( values.output, format!( - "time:{}:/tmp:cwd:call-time:{}:call-cwd:/tmp:111", + "time:{}:{temp_dir}:cwd:call-time:{}:call-cwd:{temp_dir}:111", eval_compiler_php_version(), eval_compiler_php_version() ) @@ -134,12 +138,20 @@ return function_exists("strtotime");"#, ); assert_eq!(values.get(result), FakeValue::Bool(true)); } -/// Verifies eval `microtime()` returns a plausible float timestamp by all call paths. +/// Verifies eval `microtime()` selects its return type from `as_float` on every call +/// path, and that a float timestamp is plausible. +/// +/// Only a truthy argument yields a float. The default and explicit-false forms +/// return php's `" "` string, which begins with "0." and so +/// compares below 1000000000 — verified against php 8.5: +/// `microtime() > 1000000000` and `microtime(false) > 1000000000` are both false, +/// while `microtime(true) > 1000000000` is true. This test previously asserted the +/// opposite, encoding the always-float behaviour this dispatch used to have. #[test] fn execute_program_dispatches_microtime_builtin() { let program = parse_fragment( - br#"echo microtime() > 1000000000 ? "now" : "bad"; echo ":"; -echo microtime(as_float: false) > 1000000000 ? "named" : "bad"; echo ":"; + br#"echo microtime() > 1000000000 ? "bad" : "str"; echo ":"; +echo microtime(as_float: false) > 1000000000 ? "bad" : "str"; echo ":"; echo call_user_func("microtime", true) > 1000000000 ? "call" : "bad"; echo ":"; echo call_user_func_array("microtime", ["as_float" => true]) > 1000000000 ? "array" : "bad"; echo ":"; @@ -151,7 +163,7 @@ return function_exists("microtime");"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "now:named:call:array:"); + assert_eq!(values.output, "str:str:call:array:"); assert_eq!(values.get(result), FakeValue::Bool(true)); } /// Verifies eval `hrtime()`, `http_response_code()`, and `header()` dispatch paths. @@ -215,12 +227,12 @@ fn execute_program_dispatches_stream_introspection_builtins() { $transports = stream_get_transports(); $filters = stream_get_filters(); echo count($wrappers) . ":" . $wrappers[0] . ":" . $wrappers[5] . ":"; -echo count($transports) . ":" . $transports[0] . ":" . $transports[8] . ":"; +echo count($transports) . ":" . $transports[0] . ":" . $transports[6] . ":"; echo count($filters) . ":" . $filters[2] . ":"; $call_wrappers = call_user_func("stream_get_wrappers"); echo $call_wrappers[10] . ":"; $call_transports = call_user_func_array("stream_get_transports", []); -echo $call_transports[11] . ":"; +echo $call_transports[9] . ":"; $call_filters = call_user_func_array("stream_get_filters", []); echo $call_filters[13] . ":"; echo function_exists("stream_get_wrappers"); echo function_exists("stream_get_transports"); @@ -234,7 +246,7 @@ return function_exists("stream_get_filters");"#, assert_eq!( values.output, - "11:file:https:12:tcp:tlsv1.0:14:string.rot13:glob:tlsv1.3:bzip2.decompress:11" + "11:file:https:10:tcp:tlsv1.0:14:string.rot13:glob:tlsv1.3:bzip2.decompress:11" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-magician/src/stream_resources.rs b/crates/elephc-magician/src/stream_resources.rs index 6525dfaddd..b6679d08a8 100644 --- a/crates/elephc-magician/src/stream_resources.rs +++ b/crates/elephc-magician/src/stream_resources.rs @@ -16,11 +16,14 @@ use std::ffi::c_void; use std::fs::{File, Metadata, OpenOptions}; use std::io::{self, Read, Seek, SeekFrom, Write}; use std::net::{Shutdown, TcpListener, TcpStream}; -use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd}; +#[cfg(unix)] +use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, RawFd}; #[cfg(unix)] use std::os::unix::net::UnixStream; +#[cfg(windows)] +use std::os::windows::io::{AsRawHandle, FromRawHandle}; use std::path::PathBuf; -use std::process::{Child, Command, Stdio}; +use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio}; use crate::stream_wrappers; use crate::value::RuntimeCellHandle; @@ -44,6 +47,7 @@ pub(crate) struct EvalStreamResources { directories: HashMap, filter_resources: HashSet, hash_contexts: HashMap, + process_commands: HashMap, process_children: HashMap, socket_listeners: HashMap, socket_names: HashMap, @@ -54,3 +58,33 @@ pub(crate) struct EvalStreamResources { user_wrapper_directories: HashMap, user_wrapper_streams: HashMap, } + +/// One child descriptor requested by eval `proc_open()`. +#[derive(Clone, Debug)] +pub(crate) enum EvalProcDescriptor { + /// Opens an anonymous pipe; `child_reads` follows PHP's child-side mode. + Pipe { child_reads: bool }, + /// Opens a filesystem path for the child using a PHP fopen mode. + File { path: String, mode: String }, + /// Duplicates another configured child descriptor. + Redirect(usize), +} + +/// Process id and PHP descriptor-to-parent-pipe mapping returned by eval `proc_open()`. +pub(crate) struct EvalProcOpenResult { + pub(crate) process_id: i64, + pub(crate) pipes: Vec<(i64, i64)>, +} + +/// PHP-visible snapshot returned for an eval process resource. +pub(crate) struct EvalProcessStatus { + pub(crate) cached: bool, + pub(crate) command: String, + pub(crate) exitcode: i64, + pub(crate) pid: i64, + pub(crate) running: bool, + pub(crate) signaled: bool, + pub(crate) stopped: bool, + pub(crate) stopsig: i64, + pub(crate) termsig: i64, +} diff --git a/crates/elephc-magician/src/stream_resources/file_process_opening.rs b/crates/elephc-magician/src/stream_resources/file_process_opening.rs index 7c9d8fa5b1..90131fb845 100644 --- a/crates/elephc-magician/src/stream_resources/file_process_opening.rs +++ b/crates/elephc-magician/src/stream_resources/file_process_opening.rs @@ -56,8 +56,19 @@ impl EvalStreamResources { 'w' => false, _ => return None, }; - let mut child = Command::new("/bin/sh") - .arg("-c") + #[cfg(unix)] + let mut shell = { + let mut shell = Command::new("/bin/sh"); + shell.arg("-c"); + shell + }; + #[cfg(windows)] + let mut shell = { + let mut shell = Command::new("cmd.exe"); + shell.arg("/C"); + shell + }; + let mut child = shell .arg(command) .stdin(if read_mode { Stdio::null() @@ -71,28 +82,174 @@ impl EvalStreamResources { }) .spawn() .ok()?; - let file = if read_mode { - let stdout = child.stdout.take()?; - unsafe { - // The ChildStdout pipe is converted into the File that backs - // this eval stream; no second owner keeps the fd alive. - File::from_raw_fd(stdout.into_raw_fd()) - } + let stream = if read_mode { + EvalFileStream::new_child_stdout( + child.stdout.take()?, + command.to_string(), + "r".to_string(), + ) } else { - let stdin = child.stdin.take()?; - unsafe { - // The ChildStdin pipe is converted into the File that backs - // this eval stream; dropping it before wait sends EOF. - File::from_raw_fd(stdin.into_raw_fd()) - } + EvalFileStream::new_child_stdin( + child.stdin.take()?, + command.to_string(), + "w".to_string(), + ) }; - let id = self.insert(EvalFileStream::new( - file, - command.to_string(), - if read_mode { "r" } else { "w" }.to_string(), - )); + let id = self.insert(stream); self.process_children.insert(id, child); Some(id) } + /// Starts a command with materialized child descriptors and returns both the + /// process resource and every parent-side pipe resource. + pub(crate) fn open_process( + &mut self, + command: &str, + descriptors: &[Option; 3], + cwd: Option<&str>, + env: Option<&[(String, String)]>, + bypass_shell: bool, + ) -> Option { + let mut child_handles: [Option; 3] = std::array::from_fn(|_| None); + let mut parent_pipes = Vec::new(); + for (descriptor, spec) in descriptors.iter().enumerate() { + let Some(spec) = spec else { + continue; + }; + match spec { + EvalProcDescriptor::Pipe { child_reads } => { + let (read, write) = eval_anonymous_pipe().ok()?; + let (child, parent, mode) = if *child_reads { + (read, write, "w") + } else { + (write, read, "r") + }; + child_handles[descriptor] = Some(child); + parent_pipes.push((descriptor as i64, parent, mode)); + } + EvalProcDescriptor::File { path, mode } => { + let parsed = EvalOpenMode::parse(mode)?; + child_handles[descriptor] = Some(parsed.open(path).ok()?); + } + EvalProcDescriptor::Redirect(_) => {} + } + } + for _ in 0..descriptors.len() { + let mut changed = false; + for (descriptor, spec) in descriptors.iter().enumerate() { + let Some(EvalProcDescriptor::Redirect(target)) = spec else { + continue; + }; + if child_handles[descriptor].is_none() { + if let Some(target_handle) = child_handles.get(*target)?.as_ref() { + child_handles[descriptor] = Some(target_handle.try_clone().ok()?); + changed = true; + } + } + } + if !changed { + break; + } + } + if descriptors + .iter() + .enumerate() + .any(|(index, spec)| spec.is_some() && child_handles[index].is_none()) + { + return None; + } + + let mut command_builder; + if bypass_shell { + let mut parts = command.split_whitespace(); + command_builder = Command::new(parts.next()?); + command_builder.args(parts); + } else { + #[cfg(unix)] + { + let mut shell = Command::new("/bin/sh"); + shell.arg("-c"); + command_builder = shell; + } + #[cfg(windows)] + { + let mut shell = Command::new("cmd.exe"); + shell.arg("/C"); + command_builder = shell; + } + command_builder.arg(command); + } + command_builder + .stdin(child_handles[0].take().map_or_else(Stdio::null, Stdio::from)) + .stdout(child_handles[1].take().map_or_else(Stdio::null, Stdio::from)) + .stderr(child_handles[2].take().map_or_else(Stdio::null, Stdio::from)); + if let Some(cwd) = cwd { + command_builder.current_dir(cwd); + } + if let Some(env) = env { + command_builder.env_clear().envs(env.iter().cloned()); + } + let child = command_builder.spawn().ok()?; + let id = self.next_id; + self.next_id += 1; + self.process_children.insert(id, child); + self.process_commands.insert(id, command.to_string()); + let mut pipes = Vec::with_capacity(parent_pipes.len()); + for (descriptor, parent, mode) in parent_pipes { + let pipe_id = self.insert(EvalFileStream::new( + parent, + format!("proc://{command}/{descriptor}"), + mode.to_string(), + )); + pipes.push((descriptor, pipe_id)); + } + Some(EvalProcOpenResult { + process_id: id, + pipes, + }) + } + +} + +/// Creates a parent/child anonymous byte pipe as owned file handles. +#[cfg(unix)] +fn eval_anonymous_pipe() -> io::Result<(File, File)> { + let mut fds = [-1; 2]; + if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 { + return Err(io::Error::last_os_error()); + } + Ok(unsafe { (File::from_raw_fd(fds[0]), File::from_raw_fd(fds[1])) }) +} + +/// Creates a Windows anonymous pipe; Rust's process launcher duplicates the +/// selected child end with the inheritance required for stdio. +#[cfg(windows)] +fn eval_anonymous_pipe() -> io::Result<(File, File)> { + #[repr(C)] + struct SecurityAttributes { + length: u32, + descriptor: *mut c_void, + inherit: i32, + } + #[link(name = "kernel32")] + unsafe extern "system" { + /// Creates the anonymous kernel pipe used by the child process. + fn CreatePipe( + read: *mut *mut c_void, + write: *mut *mut c_void, + attributes: *mut SecurityAttributes, + size: u32, + ) -> i32; + } + let mut read = std::ptr::null_mut(); + let mut write = std::ptr::null_mut(); + let mut attributes = SecurityAttributes { + length: std::mem::size_of::() as u32, + descriptor: std::ptr::null_mut(), + inherit: 0, + }; + if unsafe { CreatePipe(&mut read, &mut write, &mut attributes, 0) } == 0 { + return Err(io::Error::last_os_error()); + } + Ok(unsafe { (File::from_raw_handle(read), File::from_raw_handle(write)) }) } diff --git a/crates/elephc-magician/src/stream_resources/operations.rs b/crates/elephc-magician/src/stream_resources/operations.rs index e5700c7d41..6f3e43af22 100644 --- a/crates/elephc-magician/src/stream_resources/operations.rs +++ b/crates/elephc-magician/src/stream_resources/operations.rs @@ -27,6 +27,7 @@ impl EvalStreamResources { || self.socket_listeners.remove(&id).is_some(); self.socket_names.remove(&id); if let Some(mut child) = self.process_children.remove(&id) { + self.process_commands.remove(&id); let _ = child.wait(); } closed && ok @@ -56,11 +57,7 @@ impl EvalStreamResources { 2 => Shutdown::Both, _ => return Some(false), }; - let result = unsafe { - // libc shutdown only observes the borrowed descriptor and mode. - libc::shutdown(stream.file.as_raw_fd(), eval_shutdown_how(shutdown)) - }; - Some(result == 0) + Some(stream.file.shutdown(shutdown).is_ok()) } /// Allocates an eval-local stream filter resource handle. @@ -79,11 +76,68 @@ impl EvalStreamResources { /// Closes a process pipe stream and returns the child exit status. pub(crate) fn pclose(&mut self, id: i64) -> Option { let mut child = self.process_children.remove(&id)?; + self.process_commands.remove(&id); self.streams.remove(&id)?; let status = child.wait().ok()?; Some(status.code().unwrap_or(0) as i64) } + /// Waits for a process resource created by `proc_open` and returns its exit code. + pub(crate) fn close_process(&mut self, id: i64) -> Option { + let mut child = self.process_children.remove(&id)?; + self.process_commands.remove(&id); + let status = child.wait().ok()?; + Some(status.code().unwrap_or(0) as i64) + } + + /// Returns a PHP-shaped status snapshot without consuming an eval process resource. + pub(crate) fn process_status(&mut self, id: i64) -> Option { + let command = self.process_commands.get(&id)?.clone(); + let child = self.process_children.get_mut(&id)?; + let pid = i64::from(child.id()); + let status = child.try_wait().ok()?; + let running = status.is_none(); + let exitcode = status.and_then(|status| status.code()).map_or(-1, i64::from); + #[cfg(unix)] + let termsig = status + .and_then(|status| std::os::unix::process::ExitStatusExt::signal(&status)) + .map_or(0, i64::from); + #[cfg(windows)] + let termsig = 0; + #[cfg(unix)] + let cached = !running; + #[cfg(windows)] + let cached = false; + Some(EvalProcessStatus { + cached, + command, + exitcode, + pid, + running, + signaled: termsig != 0, + stopped: false, + stopsig: 0, + termsig, + }) + } + + /// Sends an eval process a requested Unix signal or terminates it through Windows. + pub(crate) fn terminate_process(&mut self, id: i64, signal: i64) -> Option { + let child = self.process_children.get_mut(&id)?; + #[cfg(unix)] + { + let signal = libc::c_int::try_from(signal).ok()?; + // `child.id()` identifies the child process owned by this table and libc only + // observes that pid while delivering the requested signal. + return Some(unsafe { libc::kill(child.id() as libc::pid_t, signal) == 0 }); + } + #[cfg(windows)] + { + let _ = signal; + Some(child.kill().is_ok()) + } + } + /// Removes a directory resource from the table. pub(crate) fn close_directory(&mut self, id: i64) -> bool { self.directories.remove(&id).is_some() @@ -186,16 +240,25 @@ impl EvalStreamResources { /// Returns whether a stream's file descriptor is attached to a terminal. pub(crate) fn isatty(&self, id: i64) -> Option { let stream = self.streams.get(&id)?; + #[cfg(unix)] let result = unsafe { // libc only reads the descriptor value during the terminal probe. libc::isatty(stream.file.as_raw_fd()) }; - Some(result == 1) + #[cfg(unix)] + return Some(result == 1); + #[cfg(windows)] + { + let _ = stream; + Some(false) + } } /// Toggles blocking mode on a stream's file descriptor. pub(crate) fn set_blocking(&self, id: i64, enable: bool) -> Option { let stream = self.streams.get(&id)?; + #[cfg(unix)] + { let fd = stream.file.as_raw_fd(); let flags = unsafe { // fcntl reads the current descriptor flags without taking ownership. @@ -214,6 +277,9 @@ impl EvalStreamResources { libc::fcntl(fd, libc::F_SETFL, flags) }; Some(result == 0) + } + #[cfg(windows)] + { Some(stream.file.set_nonblocking(!enable).is_ok()) } } /// Reports timeout-setting support for local file streams. @@ -238,15 +304,24 @@ impl EvalStreamResources { pub(crate) fn flock(&self, id: i64, operation: i64) -> Option<(bool, bool)> { let stream = self.streams.get(&id)?; let operation = eval_flock_operation(operation)?; + #[cfg(unix)] let result = unsafe { // libc only observes the borrowed raw fd during this call. libc::flock(stream.file.as_raw_fd(), operation) }; + #[cfg(windows)] + let result = stream.file.flock(i64::from(operation)); + #[cfg(unix)] if result == 0 { Some((true, false)) } else { Some((false, eval_flock_would_block())) } + #[cfg(windows)] + match result { + Ok(()) => Some((true, false)), + Err(error) => Some((false, matches!(error.raw_os_error(), Some(33 | 997)))), + } } /// Synchronizes stream data and metadata to storage. diff --git a/crates/elephc-magician/src/stream_resources/sockets.rs b/crates/elephc-magician/src/stream_resources/sockets.rs index f98928c356..57e882024a 100644 --- a/crates/elephc-magician/src/stream_resources/sockets.rs +++ b/crates/elephc-magician/src/stream_resources/sockets.rs @@ -14,6 +14,9 @@ impl EvalStreamResources { /// Opens a TCP listener resource for `stream_socket_server()`. pub(crate) fn open_tcp_listener(&mut self, address: &str) -> Option { + if eval_address_selects_tls(address) { + return None; + } let listener = TcpListener::bind(eval_tcp_address(address)).ok()?; let local = listener.local_addr().ok()?.to_string(); let id = self.next_id; @@ -36,6 +39,9 @@ impl EvalStreamResources { /// Opens a connected TCP stream resource and preserves the host I/O error on failure. pub(crate) fn open_tcp_stream_result(&mut self, address: &str) -> io::Result { + if eval_address_selects_tls(address) { + return Err(eval_tls_transport_unsupported()); + } let stream = TcpStream::connect(eval_tcp_address(address))?; self.insert_tcp_stream(stream).ok_or_else(|| { io::Error::new(io::ErrorKind::Other, "failed to track eval TCP stream") @@ -53,11 +59,10 @@ impl EvalStreamResources { host: &str, port: i64, ) -> io::Result { - let host = host - .strip_prefix("tcp://") - .or_else(|| host.strip_prefix("ssl://")) - .or_else(|| host.strip_prefix("tls://")) - .unwrap_or(host); + if eval_address_selects_tls(host) { + return Err(eval_tls_transport_unsupported()); + } + let host = host.strip_prefix("tcp://").unwrap_or(host); self.open_tcp_stream_result(&format!("{host}:{port}")) } @@ -107,9 +112,15 @@ impl EvalStreamResources { ); Some((left_id, right_id)) } - #[cfg(not(unix))] + #[cfg(windows)] { - None + let listener = TcpListener::bind("127.0.0.1:0").ok()?; + let address = listener.local_addr().ok()?; + let left = TcpStream::connect(address).ok()?; + let (right, _) = listener.accept().ok()?; + let left_id = self.insert_tcp_stream(left)?; + let right_id = self.insert_tcp_stream(right)?; + Some((left_id, right_id)) } } diff --git a/crates/elephc-magician/src/stream_resources/storage.rs b/crates/elephc-magician/src/stream_resources/storage.rs index 9d8794020d..9a100e931e 100644 --- a/crates/elephc-magician/src/stream_resources/storage.rs +++ b/crates/elephc-magician/src/stream_resources/storage.rs @@ -83,11 +83,11 @@ impl EvalStreamResources { pub(super) fn insert_tcp_stream(&mut self, stream: TcpStream) -> Option { let local = stream.local_addr().ok()?.to_string(); let peer = stream.peer_addr().ok().map(|addr| addr.to_string()); - let file = unsafe { - // The TcpStream is moved into the File-backed eval stream. - File::from_raw_fd(stream.into_raw_fd()) - }; - let id = self.insert(EvalFileStream::new(file, local.clone(), "r+".to_string())); + let id = self.insert(EvalFileStream::new_tcp( + stream, + local.clone(), + "r+".to_string(), + )); self.socket_names .insert(id, EvalSocketNames { local, peer }); Some(id) diff --git a/crates/elephc-magician/src/stream_resources/types.rs b/crates/elephc-magician/src/stream_resources/types.rs index 3b2d943117..c237f2d783 100644 --- a/crates/elephc-magician/src/stream_resources/types.rs +++ b/crates/elephc-magician/src/stream_resources/types.rs @@ -37,36 +37,75 @@ pub(super) struct EvalSocketNames { } /// Normalizes supported TCP-style stream socket addresses. +/// +/// Only the plaintext `tcp://` scheme is stripped. The crypto transports used to be +/// stripped here too, which silently turned `ssl://` and `tls://` into unencrypted +/// TCP connections; `eval_address_selects_tls` now rejects them before they reach +/// this point. pub(super) fn eval_tcp_address(address: &str) -> &str { - address - .strip_prefix("tcp://") - .or_else(|| address.strip_prefix("ssl://")) - .or_else(|| address.strip_prefix("tls://")) - .unwrap_or(address) + address.strip_prefix("tcp://").unwrap_or(address) } -/// Converts Rust's socket shutdown enum into libc constants. -pub(super) fn eval_shutdown_how(shutdown: Shutdown) -> libc::c_int { - match shutdown { - Shutdown::Read => libc::SHUT_RD, - Shutdown::Write => libc::SHUT_WR, - Shutdown::Both => libc::SHUT_RDWR, - } +/// Reports whether an address names one of PHP's crypto transports. +/// +/// php-src registers `ssl`, `sslv3`, `tls` and `tlsv1.0`..`tlsv1.3` as transports +/// that negotiate TLS inside the connect (`php_openssl_ssl_socket_factory`, +/// ext/openssl/xp_ssl.c). Scheme names are case-insensitive. `sslv2` is excluded +/// because php rejects it rather than negotiating. This mirrors the scheme set the +/// compiled path matches in `__rt_addr_tls_crypto_method`. +pub(super) fn eval_address_selects_tls(address: &str) -> bool { + let Some((scheme, _)) = address.split_once("://") else { + return false; + }; + matches!( + scheme.to_ascii_lowercase().as_str(), + "ssl" | "sslv3" | "tls" | "tlsv1.0" | "tlsv1.1" | "tlsv1.2" | "tlsv1.3" + ) +} + +/// Returns the error reported when an eval fragment asks for a crypto transport. +/// +/// The interpreter has no TLS engine: `elephc-magician` does not link the +/// `elephc-tls` bridge, so it cannot complete a handshake. Refusing is the only +/// honest answer -- returning a plaintext socket for a `tls://` address would hand +/// back an unencrypted connection under a name that promises confidentiality, which +/// is strictly worse than failing. The compiled path negotiates these transports +/// normally; only dynamic `eval()` fragments reach this fallback. +pub(super) fn eval_tls_transport_unsupported() -> io::Error { + io::Error::new( + io::ErrorKind::Unsupported, + "TLS stream transports are not available inside eval(); \ + the interpreter fallback has no TLS engine and will not open an \ + unencrypted socket for a crypto scheme", + ) } /// Converts PHP `LOCK_*` bit flags into host `flock()` flags. pub(super) fn eval_flock_operation(operation: i64) -> Option { let non_blocking = operation & 4 != 0; + #[cfg(unix)] let base = match operation & !4 { 1 => libc::LOCK_SH, 2 => libc::LOCK_EX, 3 => libc::LOCK_UN, _ => return None, }; - Some(base | if non_blocking { libc::LOCK_NB } else { 0 }) + #[cfg(windows)] + let base = match operation & !4 { + 1 => 1, + 2 => 2, + 3 => 3, + _ => return None, + }; + #[cfg(unix)] + let non_blocking_flag = libc::LOCK_NB; + #[cfg(windows)] + let non_blocking_flag = 4; + Some(base | if non_blocking { non_blocking_flag } else { 0 }) } /// Returns whether the last host `flock()` failure was a non-blocking lock miss. +#[cfg(unix)] pub(super) fn eval_flock_would_block() -> bool { let errno = std::io::Error::last_os_error().raw_os_error(); errno.is_some_and(|code| code == libc::EWOULDBLOCK || code == libc::EAGAIN) @@ -97,9 +136,214 @@ pub(super) fn eval_builtin_stream_wrapper_exists(builtins: &[&str], protocol: &s .any(|builtin| builtin.eq_ignore_ascii_case(protocol)) } +/// Host I/O object stored behind one eval stream resource. +pub(super) enum EvalStreamHandle { + File(File), + Tcp(TcpStream), + ChildStdout(ChildStdout), + ChildStdin(ChildStdin), +} + +impl Read for EvalStreamHandle { + /// Reads from handles that expose a readable byte stream. + fn read(&mut self, buffer: &mut [u8]) -> io::Result { + match self { + Self::File(handle) => handle.read(buffer), + Self::Tcp(handle) => handle.read(buffer), + Self::ChildStdout(handle) => handle.read(buffer), + Self::ChildStdin(_) => Err(io::Error::new( + io::ErrorKind::Unsupported, + "process stdin is not readable", + )), + } + } +} + +impl Write for EvalStreamHandle { + /// Writes to handles that expose a writable byte stream. + fn write(&mut self, buffer: &[u8]) -> io::Result { + match self { + Self::File(handle) => handle.write(buffer), + Self::Tcp(handle) => handle.write(buffer), + Self::ChildStdin(handle) => handle.write(buffer), + Self::ChildStdout(_) => Err(io::Error::new( + io::ErrorKind::Unsupported, + "process stdout is not writable", + )), + } + } + + /// Flushes writable handles while treating unbuffered sockets as already flushed. + fn flush(&mut self) -> io::Result<()> { + match self { + Self::File(handle) => handle.flush(), + Self::Tcp(handle) => handle.flush(), + Self::ChildStdin(handle) => handle.flush(), + Self::ChildStdout(_) => Ok(()), + } + } +} + +impl Seek for EvalStreamHandle { + /// Seeks regular files and rejects non-seekable sockets and process pipes. + fn seek(&mut self, position: SeekFrom) -> io::Result { + match self { + Self::File(handle) => handle.seek(position), + Self::Tcp(_) | Self::ChildStdout(_) | Self::ChildStdin(_) => Err(io::Error::new( + io::ErrorKind::Unsupported, + "stream is not seekable", + )), + } + } +} + +impl EvalStreamHandle { + /// Returns regular-file metadata when the resource has a file backing. + pub(super) fn metadata(&self) -> io::Result { + match self { + Self::File(handle) => handle.metadata(), + _ => Err(io::Error::new(io::ErrorKind::Unsupported, "no file metadata")), + } + } + + /// Resizes a regular file and rejects non-file resources. + pub(super) fn set_len(&self, size: u64) -> io::Result<()> { + match self { + Self::File(handle) => handle.set_len(size), + _ => Err(io::Error::new(io::ErrorKind::Unsupported, "stream is not resizable")), + } + } + + /// Synchronizes a regular file's data and metadata. + pub(super) fn sync_all(&self) -> io::Result<()> { + match self { + Self::File(handle) => handle.sync_all(), + _ => Err(io::Error::new(io::ErrorKind::Unsupported, "stream is not syncable")), + } + } + + /// Synchronizes a regular file's data. + pub(super) fn sync_data(&self) -> io::Result<()> { + match self { + Self::File(handle) => handle.sync_data(), + _ => Err(io::Error::new(io::ErrorKind::Unsupported, "stream is not syncable")), + } + } + + /// Applies socket shutdown only to TCP-backed resources. + pub(super) fn shutdown(&self, how: Shutdown) -> io::Result<()> { + match self { + Self::Tcp(handle) => handle.shutdown(how), + _ => Err(io::Error::new(io::ErrorKind::Unsupported, "stream is not a socket")), + } + } + + /// Toggles non-blocking mode only for TCP-backed resources. + #[cfg(windows)] + pub(super) fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> { + match self { + Self::Tcp(handle) => handle.set_nonblocking(nonblocking), + _ => Err(io::Error::new(io::ErrorKind::Unsupported, "stream is not a socket")), + } + } + + /// Applies PHP flock semantics to regular Windows file handles. + #[cfg(windows)] + pub(super) fn flock(&self, operation: i64) -> io::Result<()> { + use std::ffi::c_void; + + #[repr(C)] + struct Overlapped { + internal: usize, + internal_high: usize, + offset: u32, + offset_high: u32, + event: *mut c_void, + } + + #[link(name = "kernel32")] + unsafe extern "system" { + /// Acquires a shared or exclusive byte-range lock on a Windows file handle. + fn LockFileEx( + file: *mut c_void, + flags: u32, + reserved: u32, + bytes_low: u32, + bytes_high: u32, + overlapped: *mut Overlapped, + ) -> i32; + /// Releases a byte-range lock from a Windows file handle. + fn UnlockFileEx( + file: *mut c_void, + reserved: u32, + bytes_low: u32, + bytes_high: u32, + overlapped: *mut Overlapped, + ) -> i32; + } + + const LOCKFILE_FAIL_IMMEDIATELY: u32 = 0x0000_0001; + const LOCKFILE_EXCLUSIVE_LOCK: u32 = 0x0000_0002; + let Self::File(file) = self else { + return Err(io::Error::new( + io::ErrorKind::Unsupported, + "flock requires a regular file", + )); + }; + let mut overlapped = Overlapped { + internal: 0, + internal_high: 0, + offset: 0, + offset_high: 0, + event: std::ptr::null_mut(), + }; + let base = operation & !4; + let status = unsafe { + if base == 3 { + UnlockFileEx( + file.as_raw_handle(), + 0, + u32::MAX, + u32::MAX, + &mut overlapped, + ) + } else { + let mut flags = if base == 2 { LOCKFILE_EXCLUSIVE_LOCK } else { 0 }; + if operation & 4 != 0 { + flags |= LOCKFILE_FAIL_IMMEDIATELY; + } + LockFileEx( + file.as_raw_handle(), + flags, + 0, + u32::MAX, + u32::MAX, + &mut overlapped, + ) + } + }; + if status == 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } + } + + /// Returns the borrowed Unix descriptor used by terminal and lock operations. + #[cfg(unix)] + pub(super) fn as_raw_fd(&self) -> RawFd { + match self { + Self::File(handle) => handle.as_raw_fd(), + Self::Tcp(handle) => handle.as_raw_fd(), + Self::ChildStdout(handle) => handle.as_raw_fd(), + Self::ChildStdin(handle) => handle.as_raw_fd(), + } + } +} + /// File stream stored behind one eval resource id. pub(super) struct EvalFileStream { - pub(super) file: File, + pub(super) file: EvalStreamHandle, pub(super) uri: String, pub(super) mode: String, pub(super) eof: bool, @@ -120,7 +364,7 @@ impl EvalFileStream { flush_target: Option, ) -> Self { Self { - file, + file: EvalStreamHandle::File(file), uri, mode, eof: false, @@ -128,6 +372,39 @@ impl EvalFileStream { } } + /// Creates a tracked stream around a TCP socket without conflating SOCKET and HANDLE on Windows. + pub(super) fn new_tcp(stream: TcpStream, uri: String, mode: String) -> Self { + Self { + file: EvalStreamHandle::Tcp(stream), + uri, + mode, + eof: false, + flush_target: None, + } + } + + /// Creates a readable tracked stream around a child process stdout pipe. + pub(super) fn new_child_stdout(stream: ChildStdout, uri: String, mode: String) -> Self { + Self { + file: EvalStreamHandle::ChildStdout(stream), + uri, + mode, + eof: false, + flush_target: None, + } + } + + /// Creates a writable tracked stream around a child process stdin pipe. + pub(super) fn new_child_stdin(stream: ChildStdin, uri: String, mode: String) -> Self { + Self { + file: EvalStreamHandle::ChildStdin(stream), + uri, + mode, + eof: false, + flush_target: None, + } + } + /// Flushes any buffered wrapper target before the stream resource disappears. pub(super) fn finalize_on_close(mut self) -> bool { let Some(flush_target) = self.flush_target.take() else { @@ -358,3 +635,53 @@ pub(super) fn eval_tmpfile_nonce() -> u128 { .map(|duration| duration.as_nanos()) .unwrap_or(0) } + +#[cfg(test)] +mod transport_scheme_tests { + use super::{eval_address_selects_tls, eval_tcp_address}; + + /// Verifies every crypto transport php-src registers is recognised, in any case, + /// so none of them can fall through to an unencrypted socket. + #[test] + fn detects_every_crypto_scheme() { + for address in [ + "ssl://h:1", + "sslv3://h:1", + "tls://h:1", + "tlsv1.0://h:1", + "tlsv1.1://h:1", + "tlsv1.2://h:1", + "tlsv1.3://h:1", + "TLS://h:1", + "SsL://h:1", + ] { + assert!(eval_address_selects_tls(address), "{address} must select TLS"); + } + } + + /// Verifies plaintext and unknown schemes stay on the plain path. `sslv2` belongs + /// here because php rejects it outright rather than negotiating it. + #[test] + fn leaves_plain_schemes_alone() { + for address in [ + "tcp://h:1", + "udp://h:1", + "unix:///tmp/s", + "sslv2://h:1", + "tlsv1.4://h:1", + "tlsx://h:1", + "h:1", + ] { + assert!(!eval_address_selects_tls(address), "{address} must stay plain"); + } + } + + /// Verifies only the plaintext scheme is stripped, so a crypto address can never + /// be normalised into something `TcpStream::connect` would happily open. + #[test] + fn strips_only_the_plaintext_scheme() { + assert_eq!(eval_tcp_address("tcp://h:1"), "h:1"); + assert_eq!(eval_tcp_address("h:1"), "h:1"); + assert_eq!(eval_tcp_address("tls://h:1"), "tls://h:1"); + } +} diff --git a/crates/elephc-pdo/src/lib.rs b/crates/elephc-pdo/src/lib.rs index 3145d2fd08..d6ec2a14da 100644 --- a/crates/elephc-pdo/src/lib.rs +++ b/crates/elephc-pdo/src/lib.rs @@ -123,7 +123,7 @@ fn drivername_cell() -> &'static Mutex { fn store_cstr(cell: &'static Mutex, s: &str) -> *const c_char { let bytes: Vec = s.bytes().filter(|&b| b != 0).collect(); let cstr = CString::new(bytes).unwrap_or_default(); - let mut guard = cell.lock().unwrap(); + let mut guard = cell.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); *guard = cstr; guard.as_ptr() } @@ -132,7 +132,7 @@ fn store_cstr(cell: &'static Mutex, s: &str) -> *const c_char { /// to the first byte, or null for an empty buffer. Valid until the next column /// data pointer call; elephc copies it immediately through `ptr_read_string`. fn store_bytes(bytes: Vec) -> *const c_char { - let mut guard = coldata_cell().lock().unwrap(); + let mut guard = coldata_cell().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); *guard = bytes; if guard.is_empty() { std::ptr::null() @@ -173,7 +173,7 @@ fn open_conn_for_dsn(dsn: &str) -> Result { /// Registers a newly opened connection and returns the public handle ID. fn register_conn(conn: Conn) -> i64 { let id = next_id(); - conns().lock().unwrap().insert(id, conn); + conns().lock().unwrap_or_else(|poisoned| poisoned.into_inner()).insert(id, conn); id } @@ -191,8 +191,8 @@ fn open_nonpersistent_dsn(dsn: &str) -> i64 { /// Opens or reuses a process-local persistent connection for the full DSN. fn open_persistent_dsn(dsn: &str) -> i64 { - if let Some(id) = persistent_conns().lock().unwrap().get(dsn).copied() { - if conns().lock().unwrap().contains_key(&id) { + if let Some(id) = persistent_conns().lock().unwrap_or_else(|poisoned| poisoned.into_inner()).get(dsn).copied() { + if conns().lock().unwrap_or_else(|poisoned| poisoned.into_inner()).contains_key(&id) { return id; } } @@ -201,9 +201,9 @@ fn open_persistent_dsn(dsn: &str) -> i64 { let id = register_conn(conn); persistent_conns() .lock() - .unwrap() + .unwrap_or_else(|poisoned| poisoned.into_inner()) .insert(dsn.to_string(), id); - persistent_ids().lock().unwrap().insert(id); + persistent_ids().lock().unwrap_or_else(|poisoned| poisoned.into_inner()).insert(id); id } Err(msg) => { @@ -225,7 +225,7 @@ pub extern "C" fn elephc_pdo_version() -> i32 { /// `elephc_pdo_driver_name`. #[no_mangle] pub extern "C" fn elephc_pdo_driver_name(conn_id: i64) -> *const c_char { - let name = match conns().lock().unwrap().get(&conn_id) { + let name = match conns().lock().unwrap_or_else(|poisoned| poisoned.into_inner()).get(&conn_id) { Some(Conn::Sqlite(_)) => "sqlite", Some(Conn::Postgres(_)) => "pgsql", Some(Conn::Mysql(_)) => "mysql", @@ -275,20 +275,20 @@ pub unsafe extern "C" fn elephc_pdo_open_persistent( /// `elephc_pdo_open`. Valid until the next failed open. #[no_mangle] pub extern "C" fn elephc_pdo_last_open_error() -> *const c_char { - open_error_cell().lock().unwrap().as_ptr() + open_error_cell().lock().unwrap_or_else(|poisoned| poisoned.into_inner()).as_ptr() } /// Closes a connection (finalizing any SQLite statements still registered against /// it) and removes it from the table. Unknown handles are ignored. #[no_mangle] pub extern "C" fn elephc_pdo_close(conn_id: i64) { - if persistent_ids().lock().unwrap().contains(&conn_id) { + if persistent_ids().lock().unwrap_or_else(|poisoned| poisoned.into_inner()).contains(&conn_id) { return; } // The SQLite db pointer of the connection being closed, so only *its* // statements are finalized (statements from other open SQLite connections // must be left alone). `None` when the connection is PostgreSQL or unknown. - let sqlite_db = match conns().lock().unwrap().get(&conn_id) { + let sqlite_db = match conns().lock().unwrap_or_else(|poisoned| poisoned.into_inner()).get(&conn_id) { Some(Conn::Sqlite(c)) => Some(c.db), _ => None, }; @@ -297,7 +297,7 @@ pub extern "C" fn elephc_pdo_close(conn_id: i64) { // live server-side and are dropped with the client. let owned: Vec = stmts() .lock() - .unwrap() + .unwrap_or_else(|poisoned| poisoned.into_inner()) .iter() .filter_map(|(k, s)| match s { Stmt::Sqlite(st) if sqlite_db == Some(st.db) => Some(*k), @@ -307,7 +307,7 @@ pub extern "C" fn elephc_pdo_close(conn_id: i64) { }) .collect(); { - let mut guard = stmts().lock().unwrap(); + let mut guard = stmts().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); for k in owned { if let Some(Stmt::Sqlite(s)) = guard.get(&k) { s.finalize(); @@ -315,10 +315,10 @@ pub extern "C" fn elephc_pdo_close(conn_id: i64) { guard.remove(&k); } } - if let Some(Conn::Sqlite(c)) = conns().lock().unwrap().get(&conn_id) { + if let Some(Conn::Sqlite(c)) = conns().lock().unwrap_or_else(|poisoned| poisoned.into_inner()).get(&conn_id) { c.close(); } - conns().lock().unwrap().remove(&conn_id); + conns().lock().unwrap_or_else(|poisoned| poisoned.into_inner()).remove(&conn_id); } /// Runs one or more SQL statements with no result rows (`PDO::exec`). Returns the @@ -328,7 +328,7 @@ pub extern "C" fn elephc_pdo_close(conn_id: i64) { /// `sql` must point to a NUL-terminated string valid for the duration of the call. #[no_mangle] pub unsafe extern "C" fn elephc_pdo_exec(conn_id: i64, sql: *const c_char) -> i64 { - let mut guard = conns().lock().unwrap(); + let mut guard = conns().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); match guard.get_mut(&conn_id) { Some(Conn::Sqlite(c)) => c.exec(sql), Some(Conn::Postgres(c)) => match cstr_arg(sql) { @@ -350,7 +350,7 @@ pub unsafe extern "C" fn elephc_pdo_exec(conn_id: i64, sql: *const c_char) -> i6 /// `name`, when non-null, must point to a NUL-terminated string valid for the call. #[no_mangle] pub unsafe extern "C" fn elephc_pdo_last_insert_id(conn_id: i64, name: *const c_char) -> i64 { - let mut guard = conns().lock().unwrap(); + let mut guard = conns().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); match guard.get_mut(&conn_id) { Some(Conn::Sqlite(c)) => c.last_insert_id(), Some(Conn::Postgres(c)) => c.last_insert_id(cstr_arg(name)), @@ -362,7 +362,7 @@ pub unsafe extern "C" fn elephc_pdo_last_insert_id(conn_id: i64, name: *const c_ /// Returns the number of rows changed by the most recent statement. #[no_mangle] pub extern "C" fn elephc_pdo_changes(conn_id: i64) -> i64 { - let guard = conns().lock().unwrap(); + let guard = conns().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); match guard.get(&conn_id) { Some(Conn::Sqlite(c)) => c.changes(), Some(Conn::Postgres(c)) => c.changes, @@ -374,7 +374,7 @@ pub extern "C" fn elephc_pdo_changes(conn_id: i64) -> i64 { /// Begins a transaction (`PDO::beginTransaction`). Returns `1`/`0`. #[no_mangle] pub extern "C" fn elephc_pdo_begin(conn_id: i64) -> i64 { - let mut guard = conns().lock().unwrap(); + let mut guard = conns().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); match guard.get_mut(&conn_id) { Some(Conn::Sqlite(c)) => c.exec_simple(b"BEGIN"), Some(Conn::Postgres(c)) => c.exec_simple("BEGIN"), @@ -386,7 +386,7 @@ pub extern "C" fn elephc_pdo_begin(conn_id: i64) -> i64 { /// Commits the active transaction (`PDO::commit`). Returns `1`/`0`. #[no_mangle] pub extern "C" fn elephc_pdo_commit(conn_id: i64) -> i64 { - let mut guard = conns().lock().unwrap(); + let mut guard = conns().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); match guard.get_mut(&conn_id) { Some(Conn::Sqlite(c)) => c.exec_simple(b"COMMIT"), Some(Conn::Postgres(c)) => c.exec_simple("COMMIT"), @@ -398,7 +398,7 @@ pub extern "C" fn elephc_pdo_commit(conn_id: i64) -> i64 { /// Rolls back the active transaction (`PDO::rollBack`). Returns `1`/`0`. #[no_mangle] pub extern "C" fn elephc_pdo_rollback(conn_id: i64) -> i64 { - let mut guard = conns().lock().unwrap(); + let mut guard = conns().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); match guard.get_mut(&conn_id) { Some(Conn::Sqlite(c)) => c.exec_simple(b"ROLLBACK"), Some(Conn::Postgres(c)) => c.exec_simple("ROLLBACK"), @@ -410,7 +410,7 @@ pub extern "C" fn elephc_pdo_rollback(conn_id: i64) -> i64 { /// Returns the driver's result code for the connection's last operation. #[no_mangle] pub extern "C" fn elephc_pdo_errcode(conn_id: i64) -> i64 { - let guard = conns().lock().unwrap(); + let guard = conns().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); match guard.get(&conn_id) { Some(Conn::Sqlite(c)) => c.errcode(), Some(Conn::Postgres(c)) => c.errcode, @@ -424,7 +424,7 @@ pub extern "C" fn elephc_pdo_errcode(conn_id: i64) -> i64 { #[no_mangle] pub extern "C" fn elephc_pdo_errmsg(conn_id: i64) -> *const c_char { let msg = { - let guard = conns().lock().unwrap(); + let guard = conns().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); match guard.get(&conn_id) { Some(Conn::Sqlite(c)) => c.errmsg(), Some(Conn::Postgres(c)) => c.errmsg.clone(), @@ -443,7 +443,7 @@ pub extern "C" fn elephc_pdo_errmsg(conn_id: i64) -> *const c_char { #[no_mangle] pub unsafe extern "C" fn elephc_pdo_prepare(conn_id: i64, sql: *const c_char) -> i64 { let prepared: Result = { - let mut guard = conns().lock().unwrap(); + let mut guard = conns().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); match guard.get_mut(&conn_id) { Some(Conn::Sqlite(c)) => c.prepare(sql).map(Stmt::Sqlite), Some(Conn::Postgres(c)) => match cstr_arg(sql) { @@ -472,7 +472,7 @@ pub unsafe extern "C" fn elephc_pdo_prepare(conn_id: i64, sql: *const c_char) -> match prepared { Ok(stmt) => { let id = next_id(); - stmts().lock().unwrap().insert(id, stmt); + stmts().lock().unwrap_or_else(|poisoned| poisoned.into_inner()).insert(id, stmt); id } Err(()) => -1, @@ -485,7 +485,7 @@ pub unsafe extern "C" fn elephc_pdo_prepare(conn_id: i64, sql: *const c_char) -> /// `name` must point to a NUL-terminated string valid for the duration of the call. #[no_mangle] pub unsafe extern "C" fn elephc_pdo_bind_parameter_index(stmt_id: i64, name: *const c_char) -> i64 { - let guard = stmts().lock().unwrap(); + let guard = stmts().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); let Some(name) = cstr_arg(name) else { return 0; }; @@ -500,7 +500,7 @@ pub unsafe extern "C" fn elephc_pdo_bind_parameter_index(stmt_id: i64, name: *co /// Binds an integer to the 1-based placeholder `idx`. Returns `1`/`0`. #[no_mangle] pub extern "C" fn elephc_pdo_bind_int(stmt_id: i64, idx: i64, val: i64) -> i64 { - let mut guard = stmts().lock().unwrap(); + let mut guard = stmts().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); match guard.get_mut(&stmt_id) { Some(Stmt::Sqlite(s)) => s.bind_int(idx, val), Some(Stmt::Postgres(s)) => s.bind(idx, pg::Bind::Int(val)), @@ -512,7 +512,7 @@ pub extern "C" fn elephc_pdo_bind_int(stmt_id: i64, idx: i64, val: i64) -> i64 { /// Binds a double to the 1-based placeholder `idx`. Returns `1`/`0`. #[no_mangle] pub extern "C" fn elephc_pdo_bind_double(stmt_id: i64, idx: i64, val: f64) -> i64 { - let mut guard = stmts().lock().unwrap(); + let mut guard = stmts().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); match guard.get_mut(&stmt_id) { Some(Stmt::Sqlite(s)) => s.bind_double(idx, val), Some(Stmt::Postgres(s)) => s.bind(idx, pg::Bind::Float(val)), @@ -528,7 +528,7 @@ pub extern "C" fn elephc_pdo_bind_double(stmt_id: i64, idx: i64, val: f64) -> i6 /// `val`, when non-null, must point to a NUL-terminated string valid for the call. #[no_mangle] pub unsafe extern "C" fn elephc_pdo_bind_text(stmt_id: i64, idx: i64, val: *const c_char) -> i64 { - let mut guard = stmts().lock().unwrap(); + let mut guard = stmts().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); match guard.get_mut(&stmt_id) { Some(Stmt::Sqlite(s)) => s.bind_text(idx, val), Some(Stmt::Postgres(s)) => { @@ -552,7 +552,7 @@ pub unsafe extern "C" fn elephc_pdo_bind_text(stmt_id: i64, idx: i64, val: *cons /// Binds SQL NULL to the 1-based placeholder `idx`. Returns `1`/`0`. #[no_mangle] pub extern "C" fn elephc_pdo_bind_null(stmt_id: i64, idx: i64) -> i64 { - let mut guard = stmts().lock().unwrap(); + let mut guard = stmts().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); match guard.get_mut(&stmt_id) { Some(Stmt::Sqlite(s)) => s.bind_null(idx), Some(Stmt::Postgres(s)) => s.bind(idx, pg::Bind::Null), @@ -564,7 +564,7 @@ pub extern "C" fn elephc_pdo_bind_null(stmt_id: i64, idx: i64) -> i64 { /// Resets a statement, keeping its parameter bindings. Returns `1`/`0`. #[no_mangle] pub extern "C" fn elephc_pdo_reset(stmt_id: i64) -> i64 { - let mut guard = stmts().lock().unwrap(); + let mut guard = stmts().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); match guard.get_mut(&stmt_id) { Some(Stmt::Sqlite(s)) => s.reset(), Some(Stmt::Postgres(s)) => s.reset(), @@ -576,7 +576,7 @@ pub extern "C" fn elephc_pdo_reset(stmt_id: i64) -> i64 { /// Clears all parameter bindings on a statement. Returns `1`/`0`. #[no_mangle] pub extern "C" fn elephc_pdo_clear_bindings(stmt_id: i64) -> i64 { - let mut guard = stmts().lock().unwrap(); + let mut guard = stmts().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); match guard.get_mut(&stmt_id) { Some(Stmt::Sqlite(s)) => s.clear_bindings(), Some(Stmt::Postgres(s)) => s.clear_bindings(), @@ -589,12 +589,12 @@ pub extern "C" fn elephc_pdo_clear_bindings(stmt_id: i64) -> i64 { /// error. #[no_mangle] pub extern "C" fn elephc_pdo_step(stmt_id: i64) -> i64 { - let mut sguard = stmts().lock().unwrap(); + let mut sguard = stmts().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); match sguard.get_mut(&stmt_id) { Some(Stmt::Sqlite(s)) => s.step(), Some(Stmt::Postgres(s)) => { let conn_id = s.conn_id; - let mut cguard = conns().lock().unwrap(); + let mut cguard = conns().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); match cguard.get_mut(&conn_id) { Some(Conn::Postgres(c)) => s.step(c), _ => -1, @@ -602,7 +602,7 @@ pub extern "C" fn elephc_pdo_step(stmt_id: i64) -> i64 { } Some(Stmt::Mysql(s)) => { let conn_id = s.conn_id; - let mut cguard = conns().lock().unwrap(); + let mut cguard = conns().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); match cguard.get_mut(&conn_id) { Some(Conn::Mysql(c)) => s.step(c), _ => -1, @@ -615,7 +615,7 @@ pub extern "C" fn elephc_pdo_step(stmt_id: i64) -> i64 { /// Returns the number of result columns for the statement. #[no_mangle] pub extern "C" fn elephc_pdo_column_count(stmt_id: i64) -> i64 { - let guard = stmts().lock().unwrap(); + let guard = stmts().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); match guard.get(&stmt_id) { Some(Stmt::Sqlite(s)) => s.column_count(), Some(Stmt::Postgres(s)) => s.column_count(), @@ -628,7 +628,7 @@ pub extern "C" fn elephc_pdo_column_count(stmt_id: i64) -> i64 { #[no_mangle] pub extern "C" fn elephc_pdo_column_name(stmt_id: i64, i: i64) -> *const c_char { let name = { - let guard = stmts().lock().unwrap(); + let guard = stmts().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); match guard.get(&stmt_id) { Some(Stmt::Sqlite(s)) => s.column_name(i), Some(Stmt::Postgres(s)) => s.column_name(i), @@ -643,7 +643,7 @@ pub extern "C" fn elephc_pdo_column_name(stmt_id: i64, i: i64) -> *const c_char /// (0-based): 1=int, 2=float, 3=text, 4=blob/bytea, 5=null. #[no_mangle] pub extern "C" fn elephc_pdo_column_type(stmt_id: i64, i: i64) -> i64 { - let guard = stmts().lock().unwrap(); + let guard = stmts().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); match guard.get(&stmt_id) { Some(Stmt::Sqlite(s)) => s.column_type(i), Some(Stmt::Postgres(s)) => s.column_type(i), @@ -655,7 +655,7 @@ pub extern "C" fn elephc_pdo_column_type(stmt_id: i64, i: i64) -> i64 { /// Returns the current row's column `i` (0-based) as an integer. #[no_mangle] pub extern "C" fn elephc_pdo_column_int(stmt_id: i64, i: i64) -> i64 { - let guard = stmts().lock().unwrap(); + let guard = stmts().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); match guard.get(&stmt_id) { Some(Stmt::Sqlite(s)) => s.column_int(i), Some(Stmt::Postgres(s)) => s.column_int(i), @@ -667,7 +667,7 @@ pub extern "C" fn elephc_pdo_column_int(stmt_id: i64, i: i64) -> i64 { /// Returns the current row's column `i` (0-based) as a double. #[no_mangle] pub extern "C" fn elephc_pdo_column_double(stmt_id: i64, i: i64) -> f64 { - let guard = stmts().lock().unwrap(); + let guard = stmts().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); match guard.get(&stmt_id) { Some(Stmt::Sqlite(s)) => s.column_double(i), Some(Stmt::Postgres(s)) => s.column_double(i), @@ -680,7 +680,7 @@ pub extern "C" fn elephc_pdo_column_double(stmt_id: i64, i: i64) -> f64 { #[no_mangle] pub extern "C" fn elephc_pdo_column_text(stmt_id: i64, i: i64) -> *const c_char { let text = { - let guard = stmts().lock().unwrap(); + let guard = stmts().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); match guard.get(&stmt_id) { Some(Stmt::Sqlite(s)) => s.column_text(i), Some(Stmt::Postgres(s)) => s.column_text(i), @@ -696,7 +696,7 @@ pub extern "C" fn elephc_pdo_column_text(stmt_id: i64, i: i64) -> *const c_char /// NUL bytes when paired with `elephc_pdo_column_data_ptr`. #[no_mangle] pub extern "C" fn elephc_pdo_column_data_len(stmt_id: i64, i: i64) -> i64 { - let guard = stmts().lock().unwrap(); + let guard = stmts().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); match guard.get(&stmt_id) { Some(Stmt::Sqlite(s)) => s.column_data(i).len() as i64, Some(Stmt::Postgres(s)) => s.column_data(i).len() as i64, @@ -710,7 +710,7 @@ pub extern "C" fn elephc_pdo_column_data_len(stmt_id: i64, i: i64) -> i64 { #[no_mangle] pub extern "C" fn elephc_pdo_column_data_ptr(stmt_id: i64, i: i64) -> *const c_char { let bytes = { - let guard = stmts().lock().unwrap(); + let guard = stmts().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); match guard.get(&stmt_id) { Some(Stmt::Sqlite(s)) => s.column_data(i), Some(Stmt::Postgres(s)) => s.column_data(i), @@ -729,7 +729,7 @@ pub extern "C" fn elephc_pdo_column_data_byte(stmt_id: i64, i: i64, offset: i64) return 0; }; let bytes = { - let guard = stmts().lock().unwrap(); + let guard = stmts().lock().unwrap_or_else(|poisoned| poisoned.into_inner()); match guard.get(&stmt_id) { Some(Stmt::Sqlite(s)) => s.column_data(i), Some(Stmt::Postgres(s)) => s.column_data(i), @@ -744,7 +744,7 @@ pub extern "C" fn elephc_pdo_column_data_byte(stmt_id: i64, i: i64, offset: i64) /// `0`; success returns `1`. #[no_mangle] pub extern "C" fn elephc_pdo_finalize(stmt_id: i64) -> i64 { - match stmts().lock().unwrap().remove(&stmt_id) { + match stmts().lock().unwrap_or_else(|poisoned| poisoned.into_inner()).remove(&stmt_id) { Some(Stmt::Sqlite(s)) => { s.finalize(); 1 @@ -895,6 +895,89 @@ mod tests { assert_eq!(elephc_pdo_finalize(stmt), 1); } + /// A SQLite DSN containing non-ASCII path components creates and reopens + /// the same database, exercising the UTF-8 filename contract used by + /// SQLite's UTF-16 Windows VFS boundary. + #[test] + fn sqlite_unicode_path_round_trip() { + let root = std::env::temp_dir().join(format!( + "elephc-pdo-unicode-{}-Données-日本語", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir_all(&root).expect("create unicode sqlite test directory"); + let path = root.join("base-éè-東京.sqlite"); + let dsn = cs(&format!("sqlite:{}", path.display())); + let conn = unsafe { elephc_pdo_open(dsn.as_ptr()) }; + assert!(conn > 0, "unicode sqlite open failed"); + let ddl = cs("CREATE TABLE unicode_data (value TEXT)"); + assert_eq!(unsafe { elephc_pdo_exec(conn, ddl.as_ptr()) }, 0); + let insert = cs("INSERT INTO unicode_data VALUES ('été 日本語')"); + assert_eq!(unsafe { elephc_pdo_exec(conn, insert.as_ptr()) }, 1); + elephc_pdo_close(conn); + + let reopened = unsafe { elephc_pdo_open(dsn.as_ptr()) }; + assert!(reopened > 0, "unicode sqlite reopen failed"); + let sql = cs("SELECT value FROM unicode_data"); + let stmt = unsafe { elephc_pdo_prepare(reopened, sql.as_ptr()) }; + assert_eq!(elephc_pdo_step(stmt), 1); + assert_eq!(unsafe { read(elephc_pdo_column_text(stmt, 0)) }, "été 日本語"); + elephc_pdo_finalize(stmt); + elephc_pdo_close(reopened); + let _ = std::fs::remove_dir_all(root); + } + + /// SQLite transaction errors are observable: rollback removes pending + /// writes, commit persists successful writes, and an invalid extra commit + /// returns false rather than reporting a false success. + #[test] + fn sqlite_transactions_commit_rollback_and_error() { + let dsn = cs("sqlite::memory:"); + let conn = unsafe { elephc_pdo_open(dsn.as_ptr()) }; + let ddl = cs("CREATE TABLE tx (n INTEGER)"); + assert_eq!(unsafe { elephc_pdo_exec(conn, ddl.as_ptr()) }, 0); + assert_eq!(elephc_pdo_begin(conn), 1); + let first = cs("INSERT INTO tx VALUES (1)"); + assert_eq!(unsafe { elephc_pdo_exec(conn, first.as_ptr()) }, 1); + assert_eq!(elephc_pdo_rollback(conn), 1); + assert_eq!(elephc_pdo_begin(conn), 1); + let second = cs("INSERT INTO tx VALUES (2)"); + assert_eq!(unsafe { elephc_pdo_exec(conn, second.as_ptr()) }, 1); + assert_eq!(elephc_pdo_commit(conn), 1); + assert_eq!(elephc_pdo_commit(conn), 0, "commit without transaction must fail"); + assert_ne!(elephc_pdo_errcode(conn), 0); + elephc_pdo_close(conn); + } + + /// Two file-backed connections surface SQLite's lock error while one owns + /// an immediate write transaction; the blocked writer must return `-1` and + /// expose a non-zero error code instead of claiming success. + #[test] + fn sqlite_file_lock_reports_busy_error() { + let path = std::env::temp_dir().join(format!( + "elephc-pdo-lock-{}.sqlite", + std::process::id() + )); + let _ = std::fs::remove_file(&path); + let dsn = cs(&format!("sqlite:{}", path.display())); + let first = unsafe { elephc_pdo_open(dsn.as_ptr()) }; + let second = unsafe { elephc_pdo_open(dsn.as_ptr()) }; + let ddl = cs("CREATE TABLE locked (n INTEGER)"); + assert_eq!(unsafe { elephc_pdo_exec(first, ddl.as_ptr()) }, 0); + let short_timeout = cs("PRAGMA busy_timeout=1"); + assert_eq!(unsafe { elephc_pdo_exec(second, short_timeout.as_ptr()) }, 0); + let begin = cs("BEGIN IMMEDIATE"); + assert_eq!(unsafe { elephc_pdo_exec(first, begin.as_ptr()) }, 0); + let insert = cs("INSERT INTO locked VALUES (1)"); + assert_eq!(unsafe { elephc_pdo_exec(second, insert.as_ptr()) }, -1); + assert_ne!(elephc_pdo_errcode(second), 0); + let rollback = cs("ROLLBACK"); + assert_eq!(unsafe { elephc_pdo_exec(first, rollback.as_ptr()) }, 0); + elephc_pdo_close(first); + elephc_pdo_close(second); + let _ = std::fs::remove_file(path); + } + /// Placeholder translation: `?` → `$1`, `:name` → `$N` (deduped), with /// `'…'` literals and the `::` cast operator left untouched. #[test] diff --git a/crates/elephc-pdo/src/my.rs b/crates/elephc-pdo/src/my.rs index 142604c144..7b1c011179 100644 --- a/crates/elephc-pdo/src/my.rs +++ b/crates/elephc-pdo/src/my.rs @@ -502,7 +502,11 @@ impl MyStmt { let mut rows = Vec::new(); if is_select { for row in res.by_ref() { - rows.push(decode_row(row?.unwrap(), &col_kinds)); + let row = row?; + let values = (0..row.len()) + .map(|index| row.as_ref(index).cloned().unwrap_or(Value::NULL)) + .collect(); + rows.push(decode_row(values, &col_kinds)); } } let affected = res.affected_rows() as i64; diff --git a/crates/elephc-pdo/src/sqlite.rs b/crates/elephc-pdo/src/sqlite.rs index d1db60f357..85e1b3600e 100644 --- a/crates/elephc-pdo/src/sqlite.rs +++ b/crates/elephc-pdo/src/sqlite.rs @@ -11,6 +11,10 @@ //! - SQLite is statically bundled (`libsqlite3-sys`'s `bundled` feature), so a //! compiled PHP binary that links this staticlib has no system SQLite runtime //! dependency. +//! - Paths stay UTF-8 at the bridge boundary. SQLite's Windows VFS converts its +//! UTF-8 filename API to native UTF-16, preserving non-ASCII DSN paths. +//! - Connections use FULLMUTEX and a bounded busy timeout so concurrent Windows +//! file locks report a real SQLite error instead of a false success or spin. //! - Column type codes match SQLite's: 1=INTEGER, 2=FLOAT, 3=TEXT, 4=BLOB, //! 5=NULL — the same codes the PDO prelude's `columnValue()` reads. @@ -51,7 +55,7 @@ impl SqliteConn { return Err("invalid database path".to_string()); }; let mut db: *mut ffi::sqlite3 = ptr::null_mut(); - let flags = ffi::SQLITE_OPEN_READWRITE | ffi::SQLITE_OPEN_CREATE; + let flags = ffi::SQLITE_OPEN_READWRITE | ffi::SQLITE_OPEN_CREATE | ffi::SQLITE_OPEN_FULLMUTEX; let rc = unsafe { ffi::sqlite3_open_v2(c_path.as_ptr(), &mut db, flags, ptr::null()) }; if rc != ffi::SQLITE_OK { let msg = if db.is_null() { @@ -64,6 +68,13 @@ impl SqliteConn { } return Err(msg); } + let timeout_rc = unsafe { ffi::sqlite3_busy_timeout(db, 5_000) }; + if timeout_rc != ffi::SQLITE_OK { + let msg = unsafe { read_errmsg(db) }; + unsafe { ffi::sqlite3_close(db) }; + return Err(msg); + } + unsafe { ffi::sqlite3_extended_result_codes(db, 1) }; Ok(SqliteConn { db }) } diff --git a/crates/elephc-phar/Cargo.toml b/crates/elephc-phar/Cargo.toml index e415fbed71..14d0d497b6 100644 --- a/crates/elephc-phar/Cargo.toml +++ b/crates/elephc-phar/Cargo.toml @@ -17,3 +17,6 @@ sha1 = "0.10" sha2 = "0.10" md-5 = "0.10" rsa = { version = "0.9", features = ["sha1", "sha2", "pem"] } +# Supplies the ZipCrypto header nonce from the operating system CSPRNG. On +# Windows this uses the platform random provider instead of time-derived bytes. +getrandom = "0.2" diff --git a/crates/elephc-phar/src/lib.rs b/crates/elephc-phar/src/lib.rs index a2f2256024..1ae77e89d9 100644 --- a/crates/elephc-phar/src/lib.rs +++ b/crates/elephc-phar/src/lib.rs @@ -22,9 +22,7 @@ //! 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; @@ -455,7 +453,8 @@ 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`. +/// encrypted ZIP entries; an empty password clears it. Returns `1` on success +/// and `0` if the bridge catches an unexpected panic. /// /// # Safety /// `password_ptr` must be valid for `password_len` bytes unless `password_len` is zero. @@ -464,8 +463,9 @@ 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 + usize::from( + std::panic::catch_unwind(|| set_zip_password(slice(password_ptr, password_len))).is_ok(), + ) } /// C ABI wrapper around [`set_archive_compression`]. @@ -894,7 +894,7 @@ fn publish_result(bytes: Vec, out_len: *mut usize) -> *const u8 { let mut buffer = EXTRACT_BUFFER .get_or_init(|| Mutex::new(Vec::new())) .lock() - .expect("elephc_phar extract buffer poisoned"); + .unwrap_or_else(|poisoned| poisoned.into_inner()); buffer.clear(); buffer.extend_from_slice(&bytes); write_len(out_len, buffer.len()); @@ -916,7 +916,9 @@ fn write_streams() -> &'static Mutex>> { /// Allocates a write-stream slot and returns its synthetic descriptor. fn allocate_write_stream(stream: WriteStream) -> Option { - let mut streams = write_streams().lock().ok()?; + let mut streams = write_streams() + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); for (slot, current) in streams.iter_mut().enumerate() { if current.is_none() { *current = Some(stream); @@ -935,7 +937,9 @@ fn write_stream_slot(fd: usize) -> Option { /// Appends payload bytes to an open write stream. fn append_write_stream(fd: usize, data: &[u8]) -> Option { let slot = write_stream_slot(fd)?; - let mut streams = write_streams().lock().ok()?; + let mut streams = write_streams() + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); let stream = streams.get_mut(slot)?.as_mut()?; stream.payload.extend_from_slice(data); Some(data.len()) @@ -945,7 +949,9 @@ fn append_write_stream(fd: usize, data: &[u8]) -> Option { fn finalize_write_stream(fd: usize) -> Option<()> { let slot = write_stream_slot(fd)?; let stream = { - let mut streams = write_streams().lock().ok()?; + let mut streams = write_streams() + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); streams.get_mut(slot)?.take()? }; match stream.target { @@ -1659,7 +1665,7 @@ fn write_zip_entry( let password = if encrypt { current_zip_password() } else { None }; let (stored, flags) = match password { Some(pw) => ( - zipcrypto_encrypt(&pw, &stored, (crc >> 24) as u8), + zipcrypto_encrypt(&pw, &stored, (crc >> 24) as u8)?, ZIP_FLAG_ENCRYPTED, ), None => (stored, 0u16), @@ -1810,10 +1816,14 @@ fn strip_signature_trailer(archive: &[u8]) -> &[u8] { if n < 8 || &archive[n - 4..] != b"GBMB" { return archive; } - let flags = u32::from_le_bytes(archive[n - 8..n - 4].try_into().unwrap()); + let Some(flags) = le32(archive, n - 8) else { + return archive; + }; 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; + let Some(sig_len) = le32(archive, n - 12).map(|len| len as usize) else { + return archive; + }; if let Some(total) = sig_len.checked_add(12) { if n >= total { return &archive[..n - total]; @@ -2030,10 +2040,9 @@ fn read_signature_info(path: &[u8]) -> Option<(u32, Vec)> { if n < 8 || &data[n - 4..] != b"GBMB" { return None; } - let flags = u32::from_le_bytes(data[n - 8..n - 4].try_into().unwrap()); + let flags = le32(&data, n - 8)?; 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 sig_len = le32(&data, n.checked_sub(12)?)? as usize; let start = n.checked_sub(12)?.checked_sub(sig_len)?; Some((flags, data.get(start..n - 12)?.to_vec())) } else { @@ -2414,39 +2423,19 @@ fn zipcrypto_decrypt(password: &[u8], data: &[u8], check_byte: u8) -> Option Vec { +/// not round-trip correctness. Returns `None` if the operating-system CSPRNG is +/// unavailable; callers then fail the archive write instead of emitting a +/// predictable encryption header. +fn zipcrypto_encrypt(password: &[u8], data: &[u8], check_byte: u8) -> Option> { let mut header = [0u8; 12]; - header[..11].copy_from_slice(&zipcrypto_header_filler()); + getrandom::getrandom(&mut header[..11]).ok()?; 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 + Some(out) } /// Returns the password currently set for reading and writing encrypted ZIP @@ -3077,7 +3066,8 @@ mod tests { 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 enc = zipcrypto_encrypt(password, content, (crc >> 24) as u8) + .expect("test host must expose operating-system entropy"); let csz = enc.len() as u32; let usz = content.len() as u32; let mut out = Vec::new(); @@ -3206,6 +3196,26 @@ mod tests { assert_eq!(zip_local_flag(&plain, b"a.txt"), Some(0)); } + /// Verifies separate ZipCrypto writes receive independent system-random + /// headers while both ciphertexts remain decryptable with the same key. + #[test] + fn zipcrypto_headers_use_fresh_system_entropy() { + let password = b"secret"; + let payload = b"same plaintext"; + let check_byte = (crc32(payload) >> 24) as u8; + let first = zipcrypto_encrypt(password, payload, check_byte) + .expect("test host must expose operating-system entropy"); + let second = zipcrypto_encrypt(password, payload, check_byte) + .expect("test host must expose operating-system entropy"); + + assert_ne!(&first[..12], &second[..12]); + assert_eq!(zipcrypto_decrypt(password, &first, check_byte).as_deref(), Some(&payload[..])); + assert_eq!( + zipcrypto_decrypt(password, &second, check_byte).as_deref(), + Some(&payload[..]) + ); + } + /// 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 @@ -4090,4 +4100,38 @@ rNiobfy8sSb6iw==\n\ assert_eq!(set_stub_bytes(pb.as_bytes(), b" isize { + match error.kind() { + std::io::ErrorKind::WouldBlock => TLS_IO_WOULD_BLOCK, + std::io::ErrorKind::TimedOut => TLS_IO_TIMED_OUT, + _ => TLS_IO_TERMINAL, + } +} + +/// Converts a Rust TLS read error into the stable read-side ABI result. +/// +/// php-src enables OpenSSL's `SSL_OP_IGNORE_UNEXPECTED_EOF` for stream +/// clients. Rustls reports the equivalent peer TCP close without a +/// `close_notify` alert as an error, but PHP surfaces the bytes already read +/// and marks the stream at EOF. Writes retain the terminal-error mapping. +fn tls_read_error_result(error: &std::io::Error) -> isize { + if error.kind() == std::io::ErrorKind::UnexpectedEof { + 0 + } else { + tls_io_error_result(error) + } +} + +/// Returns the elephc-tls ABI version. +/// +/// v2 adds `elephc_tls_handshake`, whose tri-state result lets PHP's +/// `stream_socket_enable_crypto()` distinguish completion, nonblocking +/// progress, and failure without discarding the live rustls connection. +/// v3 adds the fixed-layout option block shared by connect and attach. #[no_mangle] pub extern "C" fn elephc_tls_version() -> i32 { - 1 + 3 } struct HandleEntry { sock: TcpStream, conn: ClientConnection, + config: Arc, } /// Returns the process-wide TLS handle table guarded by a mutex. @@ -59,99 +174,393 @@ fn next_handle_id() -> i64 { NEXT_ID.fetch_add(1, Ordering::SeqCst) } -/// `ServerCertVerifier` that accepts any certificate. Used by -/// `elephc_tls_connect_insecure` when the caller has set the -/// `ssl.verify_peer = false` stream context option. v1 trade-off: TLS still -/// encrypts the channel, but the peer identity is no longer authenticated. +/// Inserts one live TLS stream into the process handle table, returning `-1` +/// if a prior panic poisoned the mutex instead of unwinding through the C ABI. +fn insert_handle(sock: TcpStream, conn: ClientConnection, config: Arc) -> i64 { + let id = next_handle_id(); + let Ok(mut guard) = handles().lock() else { + return -1; + }; + guard.insert( + id, + Box::new(HandleEntry { + sock, + conn, + config, + }), + ); + id +} + +/// Clones the rustls client configuration retained by one live source session. +/// +/// Reusing the same `Arc` also shares rustls's client-session +/// cache, mirroring php-src's SSL context and session reuse for +/// `stream_socket_enable_crypto(..., session_stream: $source)`. +fn session_client_config(handle_id: i64) -> Option> { + if handle_id <= 0 { + return None; + } + let guard = handles().lock().ok()?; + Some(guard.get(&handle_id)?.config.clone()) +} + +/// Independent PHP-compatible peer-verification switches decoded from the C ABI. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct VerificationPolicy { + verify_peer: bool, + verify_peer_name: bool, + allow_self_signed: bool, +} + +impl VerificationPolicy { + /// Decodes the stable verification bitset, rejecting unknown future bits. + fn from_flags(flags: u32) -> Option { + if flags & !TLS_KNOWN_VERIFY_FLAGS != 0 { + return None; + } + Some(Self { + verify_peer: flags & TLS_VERIFY_PEER != 0, + verify_peer_name: flags & TLS_VERIFY_PEER_NAME != 0, + allow_self_signed: flags & TLS_ALLOW_SELF_SIGNED != 0, + }) + } +} + +/// Parsed TLS configuration shared by connect and existing-socket attachment. +#[derive(Clone, Copy, Debug)] +struct TlsOptions<'a> { + verification_flags: u32, + cafile: Option<&'a str>, + capath: Option<&'a str>, + client_cert: Option<&'a str>, + client_key: Option<&'a str>, +} + +/// Fixed-layout TLS option block shared by connect and existing-socket attach. +/// +/// The x86_64/aarch64 ABI is 88 bytes: two `u32` header fields followed by five +/// pointer/length pairs at offsets 8/16, 24/32, 40/48, 56/64, and 72/80. +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct ElephcTlsClientOptions { + /// Must equal `TLS_CLIENT_OPTIONS_ABI_VERSION`. + pub abi_version: u32, + /// Bitset composed from `TLS_VERIFY_*` and `TLS_ALLOW_SELF_SIGNED`. + pub verification_flags: u32, + /// Optional SNI and certificate-name override. + pub peer_name_ptr: *const u8, + /// Byte length of `peer_name_ptr`. + pub peer_name_len: usize, + /// Optional PEM CA bundle path. + pub cafile_ptr: *const u8, + /// Byte length of `cafile_ptr`. + pub cafile_len: usize, + /// Optional CA-directory path. + pub capath_ptr: *const u8, + /// Byte length of `capath_ptr`. + pub capath_len: usize, + /// Optional PEM client certificate path. + pub cert_ptr: *const u8, + /// Byte length of `cert_ptr`. + pub cert_len: usize, + /// Optional PEM client private-key path. + pub key_ptr: *const u8, + /// Byte length of `key_ptr`. + pub key_len: usize, +} + +impl TlsOptions<'_> { + /// Returns the secure built-in-root configuration used by legacy wrappers. + fn secure_defaults() -> Self { + Self { + verification_flags: TLS_DEFAULT_VERIFY_FLAGS, + cafile: None, + capath: None, + client_cert: None, + client_key: None, + } + } +} + +/// Certificate verifier that keeps chain validation, peer-name validation, and +/// handshake-signature validation independent, matching PHP stream options. #[derive(Debug)] -struct NoVerification; +struct PolicyVerifier { + roots: Arc, + policy: VerificationPolicy, + supported: WebPkiSupportedAlgorithms, +} -impl ServerCertVerifier for NoVerification { - /// Accepts a server certificate in the explicit insecure verifier path. +impl ServerCertVerifier for PolicyVerifier { + /// Applies the selected chain and name policy without weakening TLS + /// CertificateVerify signature checks. fn verify_server_cert( &self, - _end_entity: &CertificateDer<'_>, - _intermediates: &[CertificateDer<'_>], - _server_name: &ServerName<'_>, + end_entity: &CertificateDer<'_>, + intermediates: &[CertificateDer<'_>], + server_name: &ServerName<'_>, _ocsp: &[u8], - _now: UnixTime, + now: UnixTime, ) -> Result { + let cert = ParsedCertificate::try_from(end_entity)?; + if self.policy.verify_peer { + let chain_result = verify_server_cert_signed_by_trust_anchor( + &cert, + &self.roots, + intermediates, + now, + self.supported.all, + ); + if let Err(chain_error) = chain_result { + if !self.policy.allow_self_signed + || verify_depth_zero_self_signed( + end_entity, + &cert, + now, + self.supported.all, + ) + .is_err() + { + return Err(chain_error); + } + } + } + if self.policy.verify_peer_name { + verify_server_name(&cert, server_name)?; + } Ok(ServerCertVerified::assertion()) } - /// Accepts TLS 1.2 signatures in the explicit insecure verifier path. + /// Cryptographically validates every TLS 1.2 CertificateVerify signature, + /// including when peer-chain and peer-name checks are disabled. fn verify_tls12_signature( &self, - _message: &[u8], - _cert: &CertificateDer<'_>, - _dss: &DigitallySignedStruct, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, ) -> Result { - Ok(HandshakeSignatureValid::assertion()) + verify_tls12_signature(message, cert, dss, &self.supported) } - /// Accepts TLS 1.3 signatures in the explicit insecure verifier path. + /// Cryptographically validates every TLS 1.3 CertificateVerify signature, + /// including when peer-chain and peer-name checks are disabled. fn verify_tls13_signature( &self, - _message: &[u8], - _cert: &CertificateDer<'_>, - _dss: &DigitallySignedStruct, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, ) -> Result { - Ok(HandshakeSignatureValid::assertion()) + verify_tls13_signature(message, cert, dss, &self.supported) } - /// Reports the signature schemes accepted by the insecure verifier. + /// Reports exactly the signature schemes implemented by the ring provider. fn supported_verify_schemes(&self) -> Vec { - vec![ - SignatureScheme::RSA_PKCS1_SHA256, - SignatureScheme::RSA_PKCS1_SHA384, - SignatureScheme::RSA_PKCS1_SHA512, - SignatureScheme::ECDSA_NISTP256_SHA256, - SignatureScheme::ECDSA_NISTP384_SHA384, - SignatureScheme::RSA_PSS_SHA256, - SignatureScheme::RSA_PSS_SHA384, - SignatureScheme::RSA_PSS_SHA512, - SignatureScheme::ED25519, - ] + self.supported.supported_schemes() } } -/// Builds a rustls client configuration with certificate verification disabled. +/// Verifies that a depth-zero certificate is self-issued and signed by its own +/// key, while still enforcing validity, purpose, and supported algorithms. +/// +/// rustls-webpki has no dedicated self-signed-leaf API. Building a temporary +/// one-certificate trust store makes the public path verifier require the +/// leaf's issuer to equal its subject and verify its signature with its own +/// SPKI. Unrelated extra certificates do not change that depth-zero property. +fn verify_depth_zero_self_signed( + end_entity: &CertificateDer<'_>, + cert: &ParsedCertificate<'_>, + now: UnixTime, + supported_algs: &[&dyn rustls::pki_types::SignatureVerificationAlgorithm], +) -> Result<(), RustlsError> { + let mut self_anchor = RootCertStore::empty(); + self_anchor.add(end_entity.clone())?; + verify_server_cert_signed_by_trust_anchor(cert, &self_anchor, &[], now, supported_algs) +} + +/// Builds a rustls client configuration with chain and name verification +/// disabled while retaining TLS handshake-signature verification. fn insecure_client_config() -> Arc { static CFG: OnceLock> = OnceLock::new(); CFG.get_or_init(|| { - let _ = rustls::crypto::ring::default_provider().install_default(); - Arc::new( - ClientConfig::builder() - .dangerous() - .with_custom_certificate_verifier(Arc::new(NoVerification)) - .with_no_client_auth(), - ) + let mut options = TlsOptions::secure_defaults(); + options.verification_flags = 0; + policy_client_config(options).expect("built-in TLS configuration must be valid") }) .clone() } +/// Builds the platform-default trust store used for server authentication. +/// +/// Windows follows php-src and reads the native certificate store so +/// administrator-installed enterprise roots work. Other targets retain the +/// deterministic Mozilla-derived bundle used by the existing bridge. +fn bundled_root_store() -> RootCertStore { + let mut roots = RootCertStore::empty(); + #[cfg(windows)] + { + let native = rustls_native_certs::load_native_certs(); + roots.add_parsable_certificates(native.certs); + } + #[cfg(not(windows))] + roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); + roots +} + +/// Adds every certificate from one PEM bundle, rejecting an unreadable, +/// malformed, or certificate-free explicit `cafile`. +fn add_cafile_roots(roots: &mut RootCertStore, cafile_path: &str) -> Option<()> { + let pem = std::fs::read(cafile_path).ok()?; + let mut reader: &[u8] = &pem; + let before = roots.len(); + for cert in rustls_pemfile::certs(&mut reader) { + roots.add(cert.ok()?).ok()?; + } + (roots.len() > before).then_some(()) +} + +/// Adds certificates from PEM files in one explicit `capath`, ignoring +/// unrelated directory entries but rejecting an unreadable or empty source. +fn add_capath_roots(roots: &mut RootCertStore, capath: &str) -> Option<()> { + let before = roots.len(); + for entry in std::fs::read_dir(capath).ok()? { + let path = match entry { + Ok(entry) => entry.path(), + Err(_) => continue, + }; + if !path.is_file() { + continue; + } + let pem = match std::fs::read(path) { + Ok(pem) => pem, + Err(_) => continue, + }; + let mut reader: &[u8] = &pem; + for cert in rustls_pemfile::certs(&mut reader).flatten() { + let _ = roots.add(cert); + } + } + (roots.len() > before).then_some(()) +} + +/// Builds the trust store for one option set. Explicit `cafile` and `capath` +/// sources are additive to each other; when neither is present the bundled +/// Mozilla roots are used. +fn configured_root_store(options: TlsOptions<'_>) -> Option { + if options.cafile.is_none() && options.capath.is_none() { + return Some(bundled_root_store()); + } + let mut roots = RootCertStore::empty(); + if let Some(cafile) = options.cafile { + add_cafile_roots(&mut roots, cafile)?; + } + if let Some(capath) = options.capath { + add_capath_roots(&mut roots, capath)?; + } + Some(roots) +} + +/// Loads an unencrypted PEM client certificate chain and private key. +fn load_client_credentials( + cert_path: &str, + key_path: &str, +) -> Option<(Vec>, PrivateKeyDer<'static>)> { + let cert_pem = std::fs::read(cert_path).ok()?; + let mut cert_reader: &[u8] = &cert_pem; + let certs: Vec> = rustls_pemfile::certs(&mut cert_reader) + .collect::>() + .ok()?; + if certs.is_empty() { + return None; + } + let key_pem = std::fs::read(key_path).ok()?; + let mut key_reader: &[u8] = &key_pem; + let key = rustls_pemfile::private_key(&mut key_reader).ok().flatten()?; + Some((certs, key)) +} + +/// Builds one rustls configuration from the complete combinable PHP TLS +/// option set, using a single policy verifier for every configuration. +fn policy_client_config(options: TlsOptions<'_>) -> Option> { + policy_client_config_for_method(options, PHP_STREAM_CRYPTO_TLS_CLIENT) +} + +/// Selects the rustls-supported protocol versions requested by a PHP crypto method. +fn protocol_versions_for_crypto_method( + crypto_method: i64, +) -> Option<&'static [&'static SupportedProtocolVersion]> { + if crypto_method < 0 + || crypto_method & !PHP_STREAM_CRYPTO_KNOWN_BITS != 0 + || crypto_method & PHP_STREAM_CRYPTO_IS_CLIENT == 0 + { + return None; + } + match ( + crypto_method & PHP_STREAM_CRYPTO_TLS_1_2 != 0, + crypto_method & PHP_STREAM_CRYPTO_TLS_1_3 != 0, + ) { + (true, true) => Some(TLS_1_2_AND_1_3), + (true, false) => Some(TLS_1_2_ONLY), + (false, true) => Some(TLS_1_3_ONLY), + (false, false) => None, + } +} + +/// Builds a policy configuration restricted to the requested PHP TLS client versions. +fn policy_client_config_for_method( + options: TlsOptions<'_>, + crypto_method: i64, +) -> Option> { + let policy = VerificationPolicy::from_flags(options.verification_flags)?; + let protocol_versions = protocol_versions_for_crypto_method(crypto_method)?; + // php-src only loads cafile/capath while verify_peer is enabled. A caller + // performing name-only or fully relaxed verification must not fail because + // an otherwise unused CA path is unreadable. + let roots = Arc::new(if policy.verify_peer { + configured_root_store(options)? + } else { + RootCertStore::empty() + }); + let provider = Arc::new(rustls::crypto::ring::default_provider()); + let supported = provider.signature_verification_algorithms; + let verifier = Arc::new(PolicyVerifier { + roots, + policy, + supported, + }); + let wants_client_auth = ClientConfig::builder_with_provider(provider) + .with_protocol_versions(protocol_versions) + .ok()? + .dangerous() + .with_custom_certificate_verifier(verifier); + + match options.client_cert { + None => Some(Arc::new(wants_client_auth.with_no_client_auth())), + Some(cert_path) => { + // php-src falls back to local_cert as the private-key source when + // local_pk is absent; local_pk alone does not enable client auth. + let key_path = options.client_key.unwrap_or(cert_path); + let (certs, key) = load_client_credentials(cert_path, key_path)?; + wants_client_auth + .with_client_auth_cert(certs, key) + .ok() + .map(Arc::new) + } + } +} + /// Returns the lazily initialized default rustls client configuration. fn shared_client_config() -> Arc { static CFG: OnceLock> = OnceLock::new(); CFG.get_or_init(|| { - // Install the `ring` crypto provider once per process. Ignore the - // "already installed" error so multiple connect calls are safe. - let _ = rustls::crypto::ring::default_provider().install_default(); - - let mut roots = RootCertStore::empty(); - roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); - Arc::new( - ClientConfig::builder() - .with_root_certificates(roots) - .with_no_client_auth(), - ) + policy_client_config(TlsOptions::secure_defaults()) + .expect("built-in TLS configuration must be valid") }) .clone() } -/// Open a TLS-secured TCP connection to `host:port` and return an integer -/// handle ID, or `-1` on any failure. The handle is consumed by -/// `elephc_tls_close`. +/// Historical secure-default connect wrapper retained for ABI compatibility. +/// New runtime paths use `elephc_tls_connect_with_options`. /// /// # Safety /// @@ -166,11 +575,9 @@ pub unsafe extern "C" fn elephc_tls_connect( tls_connect_inner(host_ptr, host_len, port, shared_client_config()) } -/// Variant of `elephc_tls_connect` that uses the `dangerous_configuration` -/// rustls path with a no-op certificate verifier. Surfaced through the -/// runtime when the caller has set `ssl.verify_peer = false` on the stream -/// context. The channel is still encrypted; only the peer identity is -/// unauthenticated. +/// Historical no-chain/no-name connect wrapper retained for ABI compatibility. +/// TLS 1.2/1.3 CertificateVerify signatures remain cryptographically checked; +/// the uniform runtime expresses this policy with verification flags `0`. /// /// # Safety /// @@ -184,35 +591,48 @@ pub unsafe extern "C" fn elephc_tls_connect_insecure( tls_connect_inner(host_ptr, host_len, port, insecure_client_config()) } +/// Opens a TCP/TLS session using one uniform, combinable PHP option surface. +/// +/// `peer_name` overrides the connection host for SNI and optional name +/// verification. `cafile` and `capath` are additive trust sources. `local_cert` +/// and `local_pk` enable client authentication; when `local_pk` is absent, +/// php-src semantics load the private key from `local_cert`. +/// +/// # Safety +/// +/// `options_ptr` must point to a readable `ElephcTlsClientOptions`. Every +/// non-null option pointer must reference its paired byte length for this call. +/// Text inputs must be UTF-8. +#[no_mangle] +pub unsafe extern "C" fn elephc_tls_connect_with_options( + host_ptr: *const u8, + host_len: usize, + port: u16, + options_ptr: *const ElephcTlsClientOptions, +) -> i64 { + let Some((options, peer_name)) = tls_options_from_abi(options_ptr) else { + return -1; + }; + let Some(config) = policy_client_config(options) else { + return -1; + }; + tls_connect_inner_named(host_ptr, host_len, port, config, peer_name) +} + /// Builds a `ClientConfig` whose trust anchors come from the PEM bundle at /// `cafile_path` instead of the built-in webpki-roots. Returns `None` if the /// path is unreadable, contains no certificates, or any certificate is /// malformed — the caller then fails the connect, matching PHP's behavior when /// `ssl.cafile` cannot be loaded. Not cached: cafile connects are rare. fn cafile_client_config(cafile_path: &str) -> Option> { - let _ = rustls::crypto::ring::default_provider().install_default(); - let pem = std::fs::read(cafile_path).ok()?; - let mut roots = RootCertStore::empty(); - let mut reader: &[u8] = &pem; - for cert in rustls_pemfile::certs(&mut reader) { - roots.add(cert.ok()?).ok()?; - } - if roots.is_empty() { - return None; - } - Some(Arc::new( - ClientConfig::builder() - .with_root_certificates(roots) - .with_no_client_auth(), - )) + let mut options = TlsOptions::secure_defaults(); + options.cafile = Some(cafile_path); + policy_client_config(options) } -/// Variant of `elephc_tls_connect` that authenticates the peer against a custom -/// CA bundle (the `ssl.cafile` stream-context option) rather than the built-in -/// webpki-roots trust store. Returns an integer handle ID, or `-1` on any -/// failure (including an unreadable/empty cafile). The secure and insecure -/// connect variants ignore the trailing `cafile_*` arguments, so the runtime -/// can share one call site and just select the function pointer. +/// Historical cafile-only connect wrapper retained for ABI compatibility. +/// New runtime paths combine cafile with other options through the uniform +/// option block. Returns `-1` for an unreadable or empty bundle. /// /// # Safety /// @@ -249,43 +669,14 @@ pub unsafe extern "C" fn elephc_tls_connect_cafile( /// so the caller fails the connect — matching PHP's behavior when `ssl.capath` /// cannot supply a trust store. Not cached: capath connects are rare. fn capath_client_config(capath: &str) -> Option> { - let _ = rustls::crypto::ring::default_provider().install_default(); - let mut roots = RootCertStore::empty(); - for entry in std::fs::read_dir(capath).ok()? { - let path = match entry { - Ok(e) => e.path(), - Err(_) => continue, - }; - if !path.is_file() { - continue; - } - let pem = match std::fs::read(&path) { - Ok(b) => b, - Err(_) => continue, - }; - let mut reader: &[u8] = &pem; - // Add every certificate the file yields; ignore malformed/non-cert - // entries so a stray non-PEM file in the directory is not fatal. - for cert in rustls_pemfile::certs(&mut reader).flatten() { - let _ = roots.add(cert); - } - } - if roots.is_empty() { - return None; - } - Some(Arc::new( - ClientConfig::builder() - .with_root_certificates(roots) - .with_no_client_auth(), - )) + let mut options = TlsOptions::secure_defaults(); + options.capath = Some(capath); + policy_client_config(options) } -/// Variant of `elephc_tls_connect` that authenticates the peer against the CA -/// certificates in the directory named by the `ssl.capath` stream-context -/// option. Returns an integer handle ID, or `-1` on any failure (including an -/// unreadable/empty directory). Mirrors `elephc_tls_connect_cafile`; the -/// secure/insecure variants ignore the trailing `capath_*` args so the runtime -/// shares one call site and just selects the function pointer. +/// Historical capath-only connect wrapper retained for ABI compatibility. +/// New runtime paths combine capath with other options through the uniform +/// option block. Returns `-1` for an unreadable or empty directory. /// /// # Safety /// @@ -314,12 +705,8 @@ pub unsafe extern "C" fn elephc_tls_connect_capath( tls_connect_inner(host_ptr, host_len, port, config) } -/// Variant of `elephc_tls_connect` that authenticates the peer against the -/// built-in webpki-roots but verifies the certificate for the host named by -/// the `ssl.peer_name` stream-context option instead of the connection host. -/// Used when a program connects to one address (e.g. an IP or alternate name) -/// but the certificate is issued for a different hostname. `peer_name` is also -/// sent as the SNI server name. Returns a handle ID, or `-1` on failure. +/// Historical peer-name-only connect wrapper retained for ABI compatibility. +/// New runtime paths carry the SNI/name override in the uniform option block. /// /// # Safety /// @@ -356,33 +743,15 @@ pub unsafe extern "C" fn elephc_tls_connect_peer_name( /// `None` (the `ssl.passphrase` option cannot decrypt it in the rustls subset). /// Not cached: client-cert connects are rare and each may use a distinct key. fn client_cert_config(cert_path: &str, key_path: &str) -> Option> { - let _ = rustls::crypto::ring::default_provider().install_default(); - let cert_pem = std::fs::read(cert_path).ok()?; - let mut cert_reader: &[u8] = &cert_pem; - let certs: Vec> = rustls_pemfile::certs(&mut cert_reader) - .filter_map(|c| c.ok()) - .collect(); - if certs.is_empty() { - return None; - } - let key_pem = std::fs::read(key_path).ok()?; - let mut key_reader: &[u8] = &key_pem; - let key: PrivateKeyDer<'static> = rustls_pemfile::private_key(&mut key_reader).ok().flatten()?; - let mut roots = RootCertStore::empty(); - roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); - ClientConfig::builder() - .with_root_certificates(roots) - .with_client_auth_cert(certs, key) - .ok() - .map(Arc::new) + let mut options = TlsOptions::secure_defaults(); + options.client_cert = Some(cert_path); + options.client_key = Some(key_path); + policy_client_config(options) } -/// Variant of `elephc_tls_connect` that presents a client certificate (mutual -/// TLS) loaded from the `ssl.local_cert` / `ssl.local_pk` PEM files. Returns an -/// integer handle ID, or `-1` on any failure (including an unreadable/malformed -/// cert or key). The non-client-cert connect variants ignore the trailing -/// `cert_*`/`key_*` arguments, so the runtime can share one call site and just -/// select the function pointer. +/// Historical explicit-client-key connect wrapper retained for ABI +/// compatibility. New runtime paths combine client identity and trust/policy +/// options through `elephc_tls_connect_with_options`. /// /// # Safety /// @@ -408,12 +777,170 @@ pub unsafe extern "C" fn elephc_tls_connect_client_cert( tls_connect_inner(host_ptr, host_len, port, config) } -/// Variant of `elephc_tls_attach_fd` that presents a client certificate (mutual -/// TLS) loaded from the `ssl.local_cert` / `ssl.local_pk` PEM files when -/// promoting an already-connected TCP fd to TLS. Returns a handle ID, or `-1` -/// on dup / handshake / SNI / cert-load failure. Used by -/// `stream_socket_enable_crypto` when the active stream context carries a -/// `local_cert`. +/// Duplicates the caller's live TCP socket `fd` into a `TcpStream` that owns +/// an independent reference, so the caller's original fd remains valid for +/// its own I/O/close while the returned stream is used exclusively for TLS +/// framing. Returns `None` if the descriptor is not an IPv4/IPv6 stream +/// socket or if duplication fails. Shared by `elephc_tls_attach_fd` and +/// `elephc_tls_attach_fd_client_cert`. +/// +/// # Safety +/// +/// `fd` must refer to a connected TCP socket owned by the caller. +#[cfg(unix)] +unsafe fn dup_as_tcp_stream(fd: i64) -> Option { + let fd = i32::try_from(fd).ok()?; + // STARTTLS can be requested on any PHP stream (including php://memory and + // Unix-domain sockets), so validate the type and family before constructing + // Rust's TCP-specific owning wrapper. + let mut socket_type: libc::c_int = 0; + let mut socket_type_len = std::mem::size_of::() as libc::socklen_t; + if libc::getsockopt( + fd, + libc::SOL_SOCKET, + libc::SO_TYPE, + &mut socket_type as *mut libc::c_int as *mut libc::c_void, + &mut socket_type_len, + ) != 0 + || socket_type != libc::SOCK_STREAM + { + return None; + } + let mut socket_address: libc::sockaddr_storage = std::mem::zeroed(); + let mut socket_address_len = + std::mem::size_of::() as libc::socklen_t; + if libc::getsockname( + fd, + &mut socket_address as *mut libc::sockaddr_storage as *mut libc::sockaddr, + &mut socket_address_len, + ) != 0 + || !matches!( + socket_address.ss_family as libc::c_int, + libc::AF_INET | libc::AF_INET6 + ) + { + return None; + } + let dup_fd = libc::dup(fd); + if dup_fd < 0 { + return None; + } + Some(TcpStream::from_raw_fd(dup_fd)) +} + +/// Windows counterpart of the Unix `dup_as_tcp_stream` above. The incoming +/// value is a raw 64-bit Winsock `SOCKET`, not a CRT file descriptor. A +/// `ManuallyDrop` provides a temporary borrowed view solely so +/// `try_clone()` can ask the standard library to duplicate the socket. The +/// borrowed view never closes the caller's socket; the returned clone is the +/// independently owned socket used and eventually closed by the TLS session. +/// Winsock validates the socket type and IP family before Rust constructs the +/// TCP-specific borrowed view. Non-socket, stale, and datagram handles return +/// `None`. +/// +/// # Safety +/// +/// `fd` must be a live Winsock `SOCKET` represented without truncation in an +/// `i64`. Ownership remains with the caller. +#[cfg(windows)] +unsafe fn dup_as_tcp_stream(fd: i64) -> Option { + if fd < 0 { + return None; + } + let socket = fd as RawSocket; + let winsock_socket = socket as libc::SOCKET; + let mut socket_type: libc::c_int = 0; + let mut socket_type_len = std::mem::size_of::() as libc::c_int; + if libc::getsockopt( + winsock_socket, + WINDOWS_SOL_SOCKET, + WINDOWS_SO_TYPE, + &mut socket_type as *mut libc::c_int as *mut libc::c_char, + &mut socket_type_len, + ) != 0 + || socket_type != WINDOWS_SOCK_STREAM + { + return None; + } + + // Winsock's SOCKADDR_STORAGE is 128 bytes and aligned to 64 bits. Use an + // equivalently sized/aligned buffer so both IPv4 and IPv6 addresses fit. + let mut socket_address = [0_u64; 16]; + let mut socket_address_len = std::mem::size_of_val(&socket_address) as libc::c_int; + if libc::getsockname( + winsock_socket, + socket_address.as_mut_ptr().cast::(), + &mut socket_address_len, + ) != 0 + { + return None; + } + let family = *(socket_address.as_ptr().cast::()); + if !matches!(family, WINDOWS_AF_INET | WINDOWS_AF_INET6) { + return None; + } + + let borrowed = ManuallyDrop::new(TcpStream::from_raw_socket(socket)); + borrowed.try_clone().ok() +} + +/// Attaches TLS to an existing TCP socket using the same combinable option tail +/// as `elephc_tls_connect_with_options`. +/// +/// # Safety +/// +/// `fd` must refer to a live caller-owned TCP socket. `options_ptr` must point +/// to a readable option block whose non-null spans remain valid for this call. +/// When `crypto_method_present` is non-zero, `crypto_method` must be a supported +/// client method mask containing TLS 1.2 and/or TLS 1.3. A positive +/// `session_handle` may identify a live source TLS stream whose client context +/// and resumption cache should be reused; unknown handles fall back to a fresh +/// policy configuration. +#[no_mangle] +pub unsafe extern "C" fn elephc_tls_attach_fd_with_options( + fd: i64, + options_ptr: *const ElephcTlsClientOptions, + crypto_method: i64, + crypto_method_present: i64, + session_handle: i64, +) -> i64 { + if crypto_method_present == 0 { + return -1; + } + let Some((options, peer_name)) = tls_options_from_abi(options_ptr) else { + return -1; + }; + let Some(policy) = VerificationPolicy::from_flags(options.verification_flags) else { + return -1; + }; + if peer_name.is_none() && policy.verify_peer_name { + return -1; + } + if protocol_versions_for_crypto_method(crypto_method).is_none() { + return -1; + } + let mut config = match session_client_config(session_handle) { + Some(config) => config, + None => { + let Some(config) = policy_client_config_for_method(options, crypto_method) else { + return -1; + }; + config + } + }; + let peer_name = match peer_name { + Some(peer_name) => peer_name, + None => { + Arc::make_mut(&mut config).enable_sni = false; + "localhost" + } + }; + tls_attach_fd_inner(fd, peer_name, config) +} + +/// Historical explicit-client-key attach wrapper retained for ABI +/// compatibility. New runtime paths pass the complete option block through +/// `elephc_tls_attach_fd_with_options`. /// /// # Safety /// @@ -422,7 +949,7 @@ pub unsafe extern "C" fn elephc_tls_connect_client_cert( /// of valid UTF-8 bytes for the duration of this call. #[no_mangle] pub unsafe extern "C" fn elephc_tls_attach_fd_client_cert( - fd: i32, + fd: i64, peer_name_ptr: *const u8, peer_name_len: usize, cert_ptr: *const u8, @@ -430,9 +957,6 @@ pub unsafe extern "C" fn elephc_tls_attach_fd_client_cert( key_ptr: *const u8, key_len: usize, ) -> i64 { - if fd < 0 || peer_name_ptr.is_null() || peer_name_len == 0 { - return -1; - } let Some((cert_path, key_path)) = client_cert_paths(cert_ptr, cert_len, key_ptr, key_len) else { return -1; }; @@ -440,30 +964,10 @@ pub unsafe extern "C" fn elephc_tls_attach_fd_client_cert( Some(c) => c, None => return -1, }; - let host_bytes = std::slice::from_raw_parts(peer_name_ptr, peer_name_len); - let host = match std::str::from_utf8(host_bytes) { - Ok(s) => s, - Err(_) => return -1, - }; - let server_name: ServerName<'static> = match ServerName::try_from(host.to_string()) { - Ok(name) => name, - Err(_) => return -1, - }; - let dup_fd = libc::dup(fd); - if dup_fd < 0 { + let Some(Some(peer_name)) = optional_utf8(peer_name_ptr, peer_name_len) else { return -1; - } - let sock = TcpStream::from_raw_fd(dup_fd); - let conn = match ClientConnection::new(config, server_name) { - Ok(c) => c, - Err(_) => return -1, }; - let id = next_handle_id(); - handles() - .lock() - .unwrap() - .insert(id, Box::new(HandleEntry { sock, conn })); - id + tls_attach_fd_inner(fd, peer_name, config) } /// Validates and decodes the client-cert/key pointer pair into `&str` paths. @@ -487,6 +991,85 @@ unsafe fn client_cert_paths<'a>( Some((cert_path, key_path)) } +/// Decodes one optional UTF-8 C-ABI byte span. Zero length means absent; a +/// nonzero length requires a non-null pointer and valid UTF-8. +/// +/// # Safety +/// +/// A non-null `ptr` must reference `len` readable bytes for this call. +unsafe fn optional_utf8<'a>(ptr: *const u8, len: usize) -> Option> { + if len == 0 { + return Some(None); + } + if ptr.is_null() { + return None; + } + std::str::from_utf8(std::slice::from_raw_parts(ptr, len)) + .ok() + .map(Some) +} + +/// Decodes the option tail shared by `connect_with_options` and +/// `attach_fd_with_options`. +/// +/// # Safety +/// +/// Every non-null pointer must reference its paired length for this call. +unsafe fn tls_options_from_raw<'a>( + verification_flags: u32, + cafile_ptr: *const u8, + cafile_len: usize, + capath_ptr: *const u8, + capath_len: usize, + cert_ptr: *const u8, + cert_len: usize, + key_ptr: *const u8, + key_len: usize, +) -> Option> { + VerificationPolicy::from_flags(verification_flags)?; + let client_cert = optional_utf8(cert_ptr, cert_len)?; + let client_key = if client_cert.is_some() { + optional_utf8(key_ptr, key_len)? + } else { + None + }; + Some(TlsOptions { + verification_flags, + cafile: optional_utf8(cafile_ptr, cafile_len)?, + capath: optional_utf8(capath_ptr, capath_len)?, + client_cert, + client_key, + }) +} + +/// Decodes and version-checks the fixed-layout C option block. +/// +/// # Safety +/// +/// `options_ptr` and every non-null span it contains must remain readable for +/// the duration of this call. +unsafe fn tls_options_from_abi<'a>( + options_ptr: *const ElephcTlsClientOptions, +) -> Option<(TlsOptions<'a>, Option<&'a str>)> { + let raw = options_ptr.as_ref()?; + if raw.abi_version != TLS_CLIENT_OPTIONS_ABI_VERSION { + return None; + } + let peer_name = optional_utf8(raw.peer_name_ptr, raw.peer_name_len)?; + let options = tls_options_from_raw( + raw.verification_flags, + raw.cafile_ptr, + raw.cafile_len, + raw.capath_ptr, + raw.capath_len, + raw.cert_ptr, + raw.cert_len, + raw.key_ptr, + raw.key_len, + )?; + Some((options, peer_name)) +} + /// Creates a TCP connection, completes the TLS handshake, and stores the session handle. unsafe fn tls_connect_inner( host_ptr: *const u8, @@ -525,20 +1108,43 @@ unsafe fn tls_connect_inner_named( Ok(s) => s, Err(_) => return -1, }; - let conn = match ClientConnection::new(config, server_name) { + let conn = match ClientConnection::new(config.clone(), server_name) { Ok(c) => c, Err(_) => return -1, }; - let id = next_handle_id(); - handles() - .lock() - .unwrap() - .insert(id, Box::new(HandleEntry { sock, conn })); - id + insert_handle(sock, conn, config) +} + +/// Creates a TLS session over a duplicated caller-owned TCP socket. +/// +/// # Safety +/// +/// `fd` must refer to a connected TCP socket that remains owned by the caller. +unsafe fn tls_attach_fd_inner( + fd: i64, + peer_name: &str, + config: Arc, +) -> i64 { + if fd < 0 || peer_name.is_empty() { + return -1; + } + let server_name: ServerName<'static> = match ServerName::try_from(peer_name.to_string()) { + Ok(name) => name, + Err(_) => return -1, + }; + let Some(sock) = dup_as_tcp_stream(fd) else { + return -1; + }; + let conn = match ClientConnection::new(config.clone(), server_name) { + Ok(conn) => conn, + Err(_) => return -1, + }; + insert_handle(sock, conn, config) } /// Read up to `max_len` decrypted bytes from the TLS session into `buf_ptr`. -/// Returns the byte count, `0` on EOF, or `-1` on error / unknown handle. +/// Returns the byte count, `0` on EOF, `-1` on a terminal error / unknown +/// handle, `-2` when the operation would block, or `-3` when it timed out. /// /// # Safety /// @@ -552,20 +1158,23 @@ pub unsafe extern "C" fn elephc_tls_read( if buf_ptr.is_null() || max_len == 0 { return 0; } - let mut guard = handles().lock().unwrap(); + let Ok(mut guard) = handles().lock() else { + return TLS_IO_TERMINAL; + }; let Some(entry) = guard.get_mut(&handle_id) else { - return -1; + return TLS_IO_TERMINAL; }; let buf = std::slice::from_raw_parts_mut(buf_ptr, max_len); let mut stream = Stream::new(&mut entry.conn, &mut entry.sock); match stream.read(buf) { Ok(n) => n as isize, - Err(_) => -1, + Err(error) => tls_read_error_result(&error), } } /// Encrypt and send `len` bytes from `buf_ptr` over the TLS session. Returns -/// the byte count written, or `-1` on error / unknown handle. +/// the byte count written, `-1` on a terminal error / unknown handle, `-2` +/// when the operation would block, or `-3` when it timed out. /// /// # Safety /// @@ -579,22 +1188,22 @@ pub unsafe extern "C" fn elephc_tls_write( if buf_ptr.is_null() || len == 0 { return 0; } - let mut guard = handles().lock().unwrap(); + let Ok(mut guard) = handles().lock() else { + return TLS_IO_TERMINAL; + }; let Some(entry) = guard.get_mut(&handle_id) else { - return -1; + return TLS_IO_TERMINAL; }; let buf = std::slice::from_raw_parts(buf_ptr, len); let mut stream = Stream::new(&mut entry.conn, &mut entry.sock); match stream.write(buf) { Ok(n) => n as isize, - Err(_) => -1, + Err(error) => tls_io_error_result(&error), } } -/// Attach a TLS session to an already-connected TCP file descriptor. -/// Used by `stream_socket_enable_crypto` to promote an existing -/// `stream_socket_client("tcp://...")` socket to TLS without -/// re-establishing the TCP connection. +/// Historical secure-default attach wrapper retained for ABI compatibility. +/// New runtime paths use `elephc_tls_attach_fd_with_options`. /// /// The wrapper duplicates `fd` via `libc::dup` so it owns its own /// reference for the rustls `TcpStream`. The caller's original `fd` @@ -603,7 +1212,9 @@ pub unsafe extern "C" fn elephc_tls_write( /// socket. The elephc runtime routes subsequent fread/fwrite/fclose /// through the TLS handle instead of the bare fd. /// -/// Returns a handle ID, or `-1` on dup / handshake / SNI failure. +/// Returns a handle ID, or `-1` on socket-duplication / SNI failure. The +/// caller must then use `elephc_tls_handshake` to complete or advance the +/// negotiation. /// /// # Safety /// @@ -613,50 +1224,50 @@ pub unsafe extern "C" fn elephc_tls_write( /// for certificate-name validation by rustls. #[no_mangle] pub unsafe extern "C" fn elephc_tls_attach_fd( - fd: i32, + fd: i64, peer_name_ptr: *const u8, peer_name_len: usize, ) -> i64 { - if fd < 0 || peer_name_ptr.is_null() || peer_name_len == 0 { + let Some(Some(peer_name)) = optional_utf8(peer_name_ptr, peer_name_len) else { return -1; - } - let host_bytes = std::slice::from_raw_parts(peer_name_ptr, peer_name_len); - let host = match std::str::from_utf8(host_bytes) { - Ok(s) => s, - Err(_) => return -1, }; - let server_name: ServerName<'static> = match ServerName::try_from(host.to_string()) { - Ok(name) => name, - Err(_) => return -1, + tls_attach_fd_inner(fd, peer_name, shared_client_config()) +} + +/// Progresses the TLS handshake attached to `handle_id`. +/// +/// Returns `1` after rustls has authenticated the peer and completed the +/// handshake, `0` when a nonblocking socket would block before completion, +/// and `-1` for an unknown handle, poisoned table, or terminal I/O/TLS error. +/// A `0` result deliberately retains the session so the next PHP +/// `stream_socket_enable_crypto(..., true)` call can continue it. +#[no_mangle] +pub extern "C" fn elephc_tls_handshake(handle_id: i64) -> i32 { + let Ok(mut guard) = handles().lock() else { + return -1; }; - // dup so the rustls Stream owns its own file descriptor; closing the - // TLS handle (via elephc_tls_close) will drop the dup, leaving the - // original fd intact for the caller's fclose path. - let dup_fd = libc::dup(fd); - if dup_fd < 0 { + let Some(entry) = guard.get_mut(&handle_id) else { return -1; - } - let sock = TcpStream::from_raw_fd(dup_fd); - let conn = match ClientConnection::new(shared_client_config(), server_name) { - Ok(c) => c, - Err(_) => { - // sock drops here and closes the dup'd fd. - return -1; - } }; - let id = next_handle_id(); - handles() - .lock() - .unwrap() - .insert(id, Box::new(HandleEntry { sock, conn })); - id + if !entry.conn.is_handshaking() { + return 1; + } + + match entry.conn.complete_io(&mut entry.sock) { + Ok(_) if entry.conn.is_handshaking() => 0, + Ok(_) => 1, + Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => 0, + Err(_) => -1, + } } /// Send a TLS close_notify, drop the underlying socket, and remove the /// session from the handle table. #[no_mangle] pub extern "C" fn elephc_tls_close(handle_id: i64) { - let mut guard = handles().lock().unwrap(); + let Ok(mut guard) = handles().lock() else { + return; + }; if let Some(mut entry) = guard.remove(&handle_id) { entry.conn.send_close_notify(); let mut stream = Stream::new(&mut entry.conn, &mut entry.sock); @@ -668,10 +1279,263 @@ pub extern "C" fn elephc_tls_close(handle_id: i64) { mod tests { use super::*; - /// Verifies that the exported TLS ABI version remains at v1. + /// Verifies that the exported TLS ABI version includes uniform option entry points. + #[test] + fn version_is_v3() { + assert_eq!(elephc_tls_version(), 3); + } + + /// Verifies the C ABI bits independently select chain, name, and + /// self-signed policy and reject unrecognised bits. + #[test] + fn verification_policy_decodes_independent_bits() { + assert_eq!( + VerificationPolicy::from_flags(TLS_DEFAULT_VERIFY_FLAGS), + Some(VerificationPolicy { + verify_peer: true, + verify_peer_name: true, + allow_self_signed: false, + }), + ); + assert_eq!( + VerificationPolicy::from_flags(TLS_VERIFY_PEER_NAME | TLS_ALLOW_SELF_SIGNED), + Some(VerificationPolicy { + verify_peer: false, + verify_peer_name: true, + allow_self_signed: true, + }), + ); + assert!(VerificationPolicy::from_flags(8).is_none()); + } + + /// Verifies the option block layout consumed by x86_64/aarch64 runtime adapters. #[test] - fn version_is_v1() { - assert_eq!(elephc_tls_version(), 1); + fn client_options_c_layout_matches_runtime_contract() { + assert_eq!(std::mem::size_of::(), 88); + assert_eq!( + std::mem::offset_of!(ElephcTlsClientOptions, abi_version), + 0 + ); + assert_eq!( + std::mem::offset_of!(ElephcTlsClientOptions, verification_flags), + 4 + ); + assert_eq!( + std::mem::offset_of!(ElephcTlsClientOptions, peer_name_ptr), + 8 + ); + assert_eq!( + std::mem::offset_of!(ElephcTlsClientOptions, peer_name_len), + 16 + ); + assert_eq!( + std::mem::offset_of!(ElephcTlsClientOptions, cafile_ptr), + 24 + ); + assert_eq!( + std::mem::offset_of!(ElephcTlsClientOptions, cafile_len), + 32 + ); + assert_eq!( + std::mem::offset_of!(ElephcTlsClientOptions, capath_ptr), + 40 + ); + assert_eq!( + std::mem::offset_of!(ElephcTlsClientOptions, capath_len), + 48 + ); + assert_eq!( + std::mem::offset_of!(ElephcTlsClientOptions, cert_ptr), + 56 + ); + assert_eq!( + std::mem::offset_of!(ElephcTlsClientOptions, cert_len), + 64 + ); + assert_eq!( + std::mem::offset_of!(ElephcTlsClientOptions, key_ptr), + 72 + ); + assert_eq!( + std::mem::offset_of!(ElephcTlsClientOptions, key_len), + 80 + ); + } + + /// Verifies the uniform connect entry rejects unknown policy bits before + /// touching the network or any optional pointer. + #[test] + fn connect_options_rejects_unknown_verification_bits() { + let host = "127.0.0.1"; + let options = ElephcTlsClientOptions { + abi_version: TLS_CLIENT_OPTIONS_ABI_VERSION, + verification_flags: 8, + peer_name_ptr: std::ptr::null(), + peer_name_len: 0, + cafile_ptr: std::ptr::null(), + cafile_len: 0, + capath_ptr: std::ptr::null(), + capath_len: 0, + cert_ptr: std::ptr::null(), + cert_len: 0, + key_ptr: std::ptr::null(), + key_len: 0, + }; + let id = unsafe { + elephc_tls_connect_with_options( + host.as_ptr(), + host.len(), + 9, + &options, + ) + }; + assert_eq!(id, -1); + } + + /// Verifies an unknown session cannot be mistaken for successful TLS progress. + #[test] + fn handshake_unknown_handle_returns_minus_one() { + assert_eq!(elephc_tls_handshake(0xDEAD_BEEF), -1); + } + + /// Verifies a nonblocking socket reports retryable handshake progress and + /// keeps its rustls session installed for the next call. + #[test] + fn handshake_would_block_preserves_session() { + #[cfg(unix)] + use std::os::fd::AsRawFd; + #[cfg(windows)] + use std::os::windows::io::AsRawSocket; + + let listener = std::net::TcpListener::bind(("127.0.0.1", 0)).unwrap(); + let address = listener.local_addr().unwrap(); + let client = TcpStream::connect(address).unwrap(); + let _server = listener.accept().unwrap().0; + client.set_nonblocking(true).unwrap(); + let peer = "localhost"; + #[cfg(unix)] + let id = unsafe { elephc_tls_attach_fd(client.as_raw_fd() as i64, peer.as_ptr(), peer.len()) }; + #[cfg(windows)] + let id = unsafe { + elephc_tls_attach_fd(client.as_raw_socket() as i64, peer.as_ptr(), peer.len()) + }; + + assert!(id > 0); + assert!( + session_client_config(id).is_some(), + "a live TLS handle must retain its reusable client context" + ); + assert_eq!(elephc_tls_handshake(id), 0); + assert!(handles().lock().unwrap().contains_key(&id)); + elephc_tls_close(id); + } + + /// Verifies the platform-default trust policy contains usable anchors. + #[test] + fn bundled_trust_store_is_populated() { + #[cfg(windows)] + assert!(!bundled_root_store().is_empty()); + #[cfg(not(windows))] + assert!(bundled_root_store().len() > 100); + } + + /// Verifies PHP client crypto-method masks select only rustls-supported + /// versions and reject server, obsolete, or unknown method requests. + #[test] + fn crypto_method_selects_supported_client_protocol_versions() { + assert!(protocol_versions_for_crypto_method(0).is_none()); + assert_eq!( + protocol_versions_for_crypto_method(PHP_STREAM_CRYPTO_TLS_CLIENT) + .unwrap() + .len(), + TLS_1_2_AND_1_3.len() + ); + let tls12 = protocol_versions_for_crypto_method(33).unwrap(); + assert_eq!(tls12.len(), 1); + assert!(std::ptr::eq(tls12[0], &rustls::version::TLS12)); + let tls13 = protocol_versions_for_crypto_method(65).unwrap(); + assert_eq!(tls13.len(), 1); + assert!(std::ptr::eq(tls13[0], &rustls::version::TLS13)); + assert!(protocol_versions_for_crypto_method(32).is_none()); + assert!(protocol_versions_for_crypto_method(64).is_none()); + assert!(protocol_versions_for_crypto_method(3).is_none()); + assert!(protocol_versions_for_crypto_method(129).is_none()); + } + + /// Verifies malformed certificate/SNI names fail before socket adoption or + /// network I/O, keeping the C ABI on its `-1` sentinel contract. + #[test] + fn attach_rejects_invalid_sni_name() { + let invalid_name = "not a valid dns name"; + let id = unsafe { + elephc_tls_attach_fd(3, invalid_name.as_ptr(), invalid_name.len()) + }; + assert_eq!(id, -1); + } + + /// Verifies STARTTLS rejects a regular descriptor before Rust adopts it as a socket. + #[cfg(unix)] + #[test] + fn attach_rejects_non_socket_descriptor() { + use std::os::fd::AsRawFd; + + let file = std::fs::File::open("/dev/null").unwrap(); + assert!(unsafe { dup_as_tcp_stream(file.as_raw_fd() as i64) }.is_none()); + } + + /// Verifies STARTTLS rejects datagram sockets before Rust adopts them as TCP streams. + #[cfg(unix)] + #[test] + fn attach_rejects_non_stream_socket_descriptor() { + use std::os::fd::AsRawFd; + + let socket = std::os::unix::net::UnixDatagram::unbound().unwrap(); + assert!(unsafe { dup_as_tcp_stream(socket.as_raw_fd() as i64) }.is_none()); + } + + /// Verifies STARTTLS rejects non-IP stream sockets before TCP adoption. + #[cfg(unix)] + #[test] + fn attach_rejects_unix_stream_descriptor() { + use std::os::fd::AsRawFd; + + let (socket, _peer) = std::os::unix::net::UnixStream::pair().unwrap(); + assert!(unsafe { dup_as_tcp_stream(socket.as_raw_fd() as i64) }.is_none()); + } + + /// Verifies Windows STARTTLS duplicates a raw 64-bit Winsock `SOCKET` into + /// independently owned Rust streams without consuming the original. + #[cfg(windows)] + #[test] + fn windows_socket_adoption_preserves_original_ownership() { + use std::os::windows::io::AsRawSocket; + + let listener = std::net::TcpListener::bind(("127.0.0.1", 0)).unwrap(); + let address = listener.local_addr().unwrap(); + let server = std::thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + let mut bytes = [0_u8; 2]; + stream.read_exact(&mut bytes).unwrap(); + bytes + }); + let mut original = TcpStream::connect(address).unwrap(); + let raw_socket = original.as_raw_socket() as i64; + let mut duplicate = unsafe { dup_as_tcp_stream(raw_socket) } + .expect("a live Winsock socket must be clonable"); + original.write_all(b"a").unwrap(); + duplicate.write_all(b"b").unwrap(); + assert_eq!(server.join().unwrap(), *b"ab"); + } + + /// Verifies Windows STARTTLS rejects UDP before constructing a TCP-specific + /// borrowed view over the caller-owned Winsock socket. + #[cfg(windows)] + #[test] + fn windows_socket_adoption_rejects_udp() { + use std::os::windows::io::AsRawSocket; + + let socket = std::net::UdpSocket::bind(("127.0.0.1", 0)).unwrap(); + assert!(unsafe { dup_as_tcp_stream(socket.as_raw_socket() as i64) }.is_none()); } /// Verifies reads from an unknown TLS handle fail with -1. @@ -690,6 +1554,32 @@ mod tests { assert_eq!(n, -1); } + /// Verifies TLS reads normalize unclean peer EOF while preserving the + /// retryable and terminal result classes used by the runtime ABI. + #[test] + fn io_error_results_distinguish_would_block_and_timeout() { + assert_eq!( + tls_read_error_result(&std::io::Error::from(std::io::ErrorKind::UnexpectedEof)), + 0, + ); + assert_eq!( + tls_io_error_result(&std::io::Error::from(std::io::ErrorKind::UnexpectedEof)), + TLS_IO_TERMINAL, + ); + assert_eq!( + tls_io_error_result(&std::io::Error::from(std::io::ErrorKind::WouldBlock)), + TLS_IO_WOULD_BLOCK, + ); + assert_eq!( + tls_io_error_result(&std::io::Error::from(std::io::ErrorKind::TimedOut)), + TLS_IO_TIMED_OUT, + ); + assert_eq!( + tls_io_error_result(&std::io::Error::from(std::io::ErrorKind::ConnectionReset)), + TLS_IO_TERMINAL, + ); + } + /// Verifies closing an unknown TLS handle is harmless. #[test] fn close_unknown_handle_is_no_op() { @@ -812,6 +1702,55 @@ mod tests { ); } + /// A depth-zero self-signed server leaf with CA=false, serverAuth EKU, and + /// `selfsigned.test` SAN, used to validate the narrow allow_self_signed path. + const TEST_SELF_SIGNED_SERVER_CERT_PEM: &str = "\ +-----BEGIN CERTIFICATE----- +MIIDVTCCAj2gAwIBAgIUdYzgZUSy+xkyLYYhX75quV1Muo0wDQYJKoZIhvcNAQEL +BQAwGjEYMBYGA1UEAwwPc2VsZnNpZ25lZC50ZXN0MB4XDTI2MDcyNDA5MTUxNFoX +DTM2MDcyMTA5MTUxNFowGjEYMBYGA1UEAwwPc2VsZnNpZ25lZC50ZXN0MIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqDpJ3TCfmAyNh/slhOE+8ytnHzi+ +w4hIan0j3OamDcbhnxLqvkMK9NmeQlZn8F0yoSTDlsz7hqmfrB3+J4DTPkC71zPk +7Ka6Jwy9Jt0ctUvFhKtLMy1FC9y5GKLBYFws7BTYCKeDvLdCbLEoiK5vV26EPvWi +dVMiPVJCanrKA6VBRAmzcupLlCXvBSp688ybXLTgeJWz8Nlxm7Md4AT7c7qJo7/n +2Y1KS/Q7y5lB6Ybk4ITq6uqtBsTgn51emKJvu81oBuP13A2NcNShOjt3wR/H07KB +cvZ0nFut7vMsS6ju3DQ+jv+ekRFguIGVwau+U+PxLp6RxvRZnOgCfWNs3QIDAQAB +o4GSMIGPMB0GA1UdDgQWBBSY+M5cJiLOEpgcnBdC9TMO4Fx3cTAfBgNVHSMEGDAW +gBSY+M5cJiLOEpgcnBdC9TMO4Fx3cTAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQE +AwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAaBgNVHREEEzARgg9zZWxmc2lnbmVk +LnRlc3QwDQYJKoZIhvcNAQELBQADggEBAB5/dtNVauwCRp1KSWjVijq4K8dQLqZF +d2etZP1Ka0OWNzHCakGV2pCGgcZ60jj8a5b95mR+mQf1LN+s5TnzMilP20eLdgW7 +fMroazs1SnEOZ/ElSABfR/BTfsSp2fm8PWppavoj5wOHSfWYq56z4DZC4EBxcYPU +MTh6oDwwRdDwkOc69nhoehAckMg6pMr/Ci522CfDW99Tq3dyv0/h6a8ijfg0l7QE +ZBT0YvX/20gSTQagNJIZg+b6g+IyZvZuXun4CnqTb0ob4bOvXxEA/58HZQGkb9jP +YAK54Ac858ia4r2WTk4QiSo0kRAHs+cVxNvyeTgyVV4ycRwgypT7VWc= +-----END CERTIFICATE----- +"; + + /// A leaf signed by a distinct test CA, presented without that CA, used to + /// prove allow_self_signed cannot turn an arbitrary leaf into its own anchor. + const TEST_CA_SIGNED_SERVER_CERT_PEM: &str = "\ +-----BEGIN CERTIFICATE----- +MIIDAjCCAeqgAwIBAgIUZl/TmUB6I7YV7IFtYCsqcc/qb5kwDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAwwOZWxlcGhjLXRlc3QtY2EwHhcNMjYwNzI0MDkxOTUwWhcN +MzYwNzIxMDkxOTUwWjAZMRcwFQYDVQQDDA5jYS1zaWduZWQudGVzdDCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBANsTLmidtKeWo6MC39rGo/g8rWpUIqzb ++26vLUDpRovlYHm2MK5eEPDXGrqWxxubH0GT0Ua4sV9812/V23hHBUzM0oBlYAme +HYkpYJub47KoZpvEG5cZq1uzBcphTZAX5vRrLCzxdYYLyBIsgfkhFQFN/I1kHuK8 +hHHHgVqT2bQWyUFM7vJ/t5GGnM9gFcIObgg03thE27DU3Vv0AmZYJKgMKfZVfg3t +1r/R3tiisFvqj8cyJNDogFTWy1FjCXMuRlRJFVWDeswEAUuFQpLt9x4rnQKX2R7G +UdXEDBjYwokKPX+qznChwJdxLadzJt0CQrNeu5mSqiEaYY6jpQmQ168CAwEAAaNC +MEAwHQYDVR0OBBYEFHMU9kY9m8TpUR1OnlKQLvvh4TjFMB8GA1UdIwQYMBaAFACe +ZsNYtl/ZFHEY2Zz2AkKB94cJMA0GCSqGSIb3DQEBCwUAA4IBAQAHt9j7kWmjMBSs +v/BXDJTH4RKx3jZj44u6qRalm3zzjboq4uN26ZSBFRnL+f/Tmb7VJYgcqA28aRKW +vuT2FnE6jdhOyz9h2lvMc+BTltfGStQbIkwoso5pbw3JWnPlxQK1K3G/r8nDBcbm +U5jj/FhO1e3H+u00nJZWqGafEMbBH7atJ1uiOM4pjHmWqK5zwJ++MwEATH2bDvh9 +28V1qKnb/9ufWkM1mV79rDZNRtY6YCu9p1pGCU/lDjH+cIcsAy5jmsedA3kYCZM7 +TkL58PKfwrEu7uYhU6/iajFyD0Fnl/1vKszLSo/fUfwGdEPAqoXcU/OukI9B0r8W +hylNBvLT +-----END CERTIFICATE----- +"; + /// A throwaway self-signed certificate and its unencrypted PKCS#8 key, used /// to exercise the `ssl.local_cert` / `ssl.local_pk` client-auth config path /// without a network round-trip. Generated with @@ -869,6 +1808,146 @@ MYQgldWOaXCRMQsHgapn+orK7iF89zA+4UDACVNiHEYS9q8CGynLckruklWdiyi3 -----END PRIVATE KEY----- "; + /// Parses the first certificate from one test PEM fixture. + fn parse_test_certificate(pem: &'static str) -> CertificateDer<'static> { + let mut reader = pem.as_bytes(); + let certificate = rustls_pemfile::certs(&mut reader) + .next() + .expect("test PEM must contain a certificate") + .expect("test certificate DER must be valid"); + certificate + } + + /// Constructs a verifier with deterministic ring algorithms for policy tests. + fn test_policy_verifier(flags: u32, roots: RootCertStore) -> PolicyVerifier { + let provider = rustls::crypto::ring::default_provider(); + PolicyVerifier { + roots: Arc::new(roots), + policy: VerificationPolicy::from_flags(flags).unwrap(), + supported: provider.signature_verification_algorithms, + } + } + + /// Verifies allow_self_signed accepts only a cryptographically self-signed + /// depth-zero server leaf and keeps peer-name validation independent. + #[test] + fn policy_verifier_limits_self_signed_fallback_to_depth_zero() { + let cert = parse_test_certificate(TEST_SELF_SIGNED_SERVER_CERT_PEM); + let valid_name = ServerName::try_from("selfsigned.test").unwrap(); + let wrong_name = ServerName::try_from("wrong.test").unwrap(); + let now = UnixTime::since_unix_epoch(std::time::Duration::from_secs(1_800_000_000)); + + let strict = test_policy_verifier(TLS_DEFAULT_VERIFY_FLAGS, RootCertStore::empty()); + assert!( + strict + .verify_server_cert(&cert, &[], &valid_name, &[], now) + .is_err(), + "an untrusted self-signed leaf must fail without the explicit option", + ); + + let relaxed = test_policy_verifier( + TLS_DEFAULT_VERIFY_FLAGS | TLS_ALLOW_SELF_SIGNED, + RootCertStore::empty(), + ); + assert!( + relaxed + .verify_server_cert(&cert, &[], &valid_name, &[], now) + .is_ok(), + "a valid self-signed depth-zero leaf must be accepted", + ); + assert!( + relaxed + .verify_server_cert(&cert, &[], &wrong_name, &[], now) + .is_err(), + "allow_self_signed must not disable peer-name verification", + ); + assert!( + relaxed + .verify_server_cert(&cert, &[cert.clone()], &valid_name, &[], now) + .is_ok(), + "extra presented certificates must not change a depth-zero self-signed leaf", + ); + + let ca_signed_leaf = parse_test_certificate(TEST_CA_SIGNED_SERVER_CERT_PEM); + assert!( + relaxed + .verify_server_cert(&ca_signed_leaf, &[], &valid_name, &[], now) + .is_err(), + "a CA-signed leaf without its issuer must not become self-trusted", + ); + + let chain_only = test_policy_verifier( + TLS_VERIFY_PEER | TLS_ALLOW_SELF_SIGNED, + RootCertStore::empty(), + ); + assert!( + chain_only + .verify_server_cert(&cert, &[], &wrong_name, &[], now) + .is_ok(), + "verify_peer_name=false must not perform a hidden name check", + ); + } + + /// Verifies cafile, capath, and a combined local_cert/key PEM can be + /// configured together, including php-src's absent-local_pk fallback. + #[test] + fn policy_config_combines_trust_sources_and_local_cert_key_fallback() { + let base = std::env::temp_dir().join(format!( + "elephc_tls_combined_options_{}", + std::process::id() + )); + let capath = base.join("ca"); + std::fs::create_dir_all(&capath).unwrap(); + let cafile = base.join("cafile.pem"); + std::fs::write(&cafile, TEST_SELF_SIGNED_SERVER_CERT_PEM).unwrap(); + std::fs::write( + capath.join("anchor.pem"), + TEST_SELF_SIGNED_SERVER_CERT_PEM, + ) + .unwrap(); + let combined_identity = base.join("identity.pem"); + std::fs::write( + &combined_identity, + format!("{TEST_CLIENT_CERT_PEM}{TEST_CLIENT_KEY_PEM}"), + ) + .unwrap(); + + let config = policy_client_config(TlsOptions { + verification_flags: TLS_DEFAULT_VERIFY_FLAGS, + cafile: cafile.to_str(), + capath: capath.to_str(), + client_cert: combined_identity.to_str(), + client_key: None, + }); + let _ = std::fs::remove_dir_all(&base); + assert!( + config.is_some(), + "all trust and client-auth sources must remain combinable", + ); + } + + /// Verifies local_pk without local_cert is ignored like php-src instead of + /// turning on partial client authentication. + #[test] + fn policy_config_ignores_local_pk_without_local_cert() { + let mut options = TlsOptions::secure_defaults(); + options.client_key = Some("/nonexistent/key-without-local-cert.pem"); + assert!(policy_client_config(options).is_some()); + } + + /// Verifies disabled chain validation does not read unused CA paths, matching php-src. + #[test] + fn policy_config_ignores_ca_paths_when_verify_peer_is_disabled() { + let options = TlsOptions { + verification_flags: TLS_VERIFY_PEER_NAME, + cafile: Some("/nonexistent/unused-cafile.pem"), + capath: Some("/nonexistent/unused-capath"), + client_cert: None, + client_key: None, + }; + assert!(policy_client_config(options).is_some()); + } + /// `client_cert_config` builds a usable mutual-TLS `ClientConfig` from a /// valid PEM certificate chain and unencrypted private key — the /// `ssl.local_cert` / `ssl.local_pk` happy path. diff --git a/crates/elephc-tz/src/abi.rs b/crates/elephc-tz/src/abi.rs index 16bbb5936d..6d2fe4f6ae 100644 --- a/crates/elephc-tz/src/abi.rs +++ b/crates/elephc-tz/src/abi.rs @@ -14,13 +14,26 @@ //! single-threaded and copies the bytes immediately, mirroring the PDO bridge. //! - An empty return marks "no data" (a false-zone or unknown name), since every //! present location/transition serialization is non-empty. +//! - Every export contains Rust panics at the ABI boundary. String exports return +//! a stable empty C string on failure, the offset export returns `i64::MIN`, and +//! poisoned result-buffer mutexes are recovered instead of propagated. +//! - `elephc_tz_offset` is a separate, windows-only offset resolver (not part of +//! the introspection surface above): it is published into a runtime +//! function-pointer slot (Mechanism A, mirroring elephc-crypto's `hash()` +//! entry points) rather than declared through the `extern "elephc_tz"` PHP +//! block the introspection methods use (Mechanism B), because its callers are +//! hand-written `__rt_sys_localtime`/`__rt_sys_mktime` runtime helpers, not PHP +//! source. See `crate::codegen_support::tz_bridge` in the main crate. +//! - `elephc_tz_abbreviation` complements that scalar bridge with a pointer into +//! its own stable mutex-backed cell, valid until the next abbreviation lookup. use std::borrow::Cow; use std::ffi::{CStr, CString}; use std::os::raw::c_char; +use std::panic::{catch_unwind, AssertUnwindSafe}; use std::sync::{Mutex, OnceLock}; -use crate::{abbreviations, zone_location, zone_transitions}; +use crate::{abbreviations, zone_location, zone_offset_at, zone_transitions}; /// Reads a borrowed zone name from a NUL-terminated C string — the way elephc /// lowers an extern `string` argument (a single `char*`). A null pointer yields @@ -38,11 +51,28 @@ unsafe fn zone_name<'a>(ptr: *const c_char) -> Cow<'a, str> { /// `String` carrying an interior NUL (never produced here) degrades to empty. fn stash(cell: &'static Mutex, s: String) -> *const c_char { let value = CString::new(s).unwrap_or_default(); - let mut guard = cell.lock().expect("tz bridge buffer mutex poisoned"); + let mut guard = cell.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); *guard = value; guard.as_ptr() } +/// Runs one string-returning C export behind an unwind barrier and stores its +/// result in the export's stable buffer. A panic becomes a valid empty C string, +/// matching the existing false-zone/error sentinel instead of crossing the ABI. +fn catch_string_export( + cell: &'static Mutex, + body: impl FnOnce() -> String, +) -> *const c_char { + let value = catch_unwind(AssertUnwindSafe(body)).unwrap_or_default(); + stash(cell, value) +} + +/// Runs one scalar C export behind an unwind barrier. A panic becomes the +/// supplied sentinel so Rust unwinding never reaches foreign callers. +fn catch_scalar_export(body: impl FnOnce() -> i64, sentinel: i64) -> i64 { + catch_unwind(AssertUnwindSafe(body)).unwrap_or(sentinel) +} + /// Serializes one zone's transitions as `ts\toffset\tdst\tabbr\ttime` rows joined /// by `\n`, or the empty string for a false-zone/unknown name. fn serialize_transitions(name: &str) -> String { @@ -126,6 +156,12 @@ fn abbreviations_cell() -> &'static Mutex { CELL.get_or_init(|| Mutex::new(CString::default())) } +/// Returns the process-wide buffer cell for one transition abbreviation. +fn abbreviation_cell() -> &'static Mutex { + static CELL: OnceLock> = OnceLock::new(); + CELL.get_or_init(|| Mutex::new(CString::default())) +} + /// C ABI: returns a zone's `getTransitions()` rows serialized as /// `ts\toffset\tdst\tabbr\ttime` lines, or an empty string for a false-zone or /// unknown name (which the marshalling turns into PHP `false`). @@ -134,8 +170,10 @@ fn abbreviations_cell() -> &'static Mutex { /// `name` must be a valid NUL-terminated C string, or null. #[no_mangle] pub unsafe extern "C" fn elephc_tz_transitions(name: *const c_char) -> *const c_char { - let name = zone_name(name); - stash(transitions_cell(), serialize_transitions(&name)) + catch_string_export(transitions_cell(), || { + let name = zone_name(name); + serialize_transitions(&name) + }) } /// C ABI: returns a zone's `getLocation()` data serialized as @@ -145,15 +183,63 @@ pub unsafe extern "C" fn elephc_tz_transitions(name: *const c_char) -> *const c_ /// `name` must be a valid NUL-terminated C string, or null. #[no_mangle] pub unsafe extern "C" fn elephc_tz_location(name: *const c_char) -> *const c_char { - let name = zone_name(name); - stash(location_cell(), serialize_location(&name)) + catch_string_export(location_cell(), || { + let name = zone_name(name); + serialize_location(&name) + }) } /// C ABI: returns the whole `listAbbreviations()` table serialized as /// `abbr\t::;...` lines in PHP order. Takes no argument. #[no_mangle] pub extern "C" fn elephc_tz_abbreviations() -> *const c_char { - stash(abbreviations_cell(), serialize_abbreviations()) + catch_string_export(abbreviations_cell(), serialize_abbreviations) +} + +/// C ABI: resolves `name`'s UTC offset (and DST flag) at Unix timestamp `ts` via +/// [`crate::zone_offset_at`], for the windows-only local-time bridge. Packs both +/// fields into one `i64`: `offset_seconds * 2 + (is_dst as i64)`, recoverable as +/// `let isdst = packed & 1; let offset = (packed - isdst) >> 1;` — exact, since +/// `packed - isdst` is always even and a real UTC offset is a small, exactly +/// representable multiple of a minute. Returns `i64::MIN` — never a legitimate +/// packed value, since real offsets are many orders of magnitude smaller — when +/// `name` is unknown or a false-zone (no transition data), telling the caller to +/// fall back to its own (non-bridge) offset resolution. +/// +/// # Safety +/// `name` must be a valid NUL-terminated C string, or null. +#[no_mangle] +pub unsafe extern "C" fn elephc_tz_offset(name: *const c_char, ts: i64) -> i64 { + catch_scalar_export( + || { + let name = zone_name(name); + match zone_offset_at(&name, ts) { + Some((offset, isdst, _abbr)) => (offset as i64) * 2 + i64::from(isdst), + None => i64::MIN, + } + }, + i64::MIN, + ) +} + +/// C ABI: resolves the active transition abbreviation for `name` at `ts`. +/// +/// The returned NUL-terminated pointer remains valid until the next call to +/// this function. Unknown and false zones return a stable empty string. +/// +/// # Safety +/// `name` must be a valid NUL-terminated C string, or null. +#[no_mangle] +pub unsafe extern "C" fn elephc_tz_abbreviation( + name: *const c_char, + ts: i64, +) -> *const c_char { + catch_string_export(abbreviation_cell(), || { + let name = zone_name(name); + zone_offset_at(&name, ts) + .map(|(_, _, abbreviation)| abbreviation) + .unwrap_or_default() + }) } #[cfg(test)] @@ -192,6 +278,24 @@ mod tests { assert!(serialize_location("CET").is_empty()); } + /// The C string exports expose the same non-null empty sentinel for unknown + /// transition and location zones, including a null input pointer. + #[test] + fn string_exports_use_valid_empty_sentinel() { + let unknown = CString::new("Not/AZone").unwrap(); + let transitions = unsafe { elephc_tz_transitions(unknown.as_ptr()) }; + assert!(!transitions.is_null()); + assert_eq!(unsafe { CStr::from_ptr(transitions) }.to_bytes(), b""); + + let location = unsafe { elephc_tz_location(std::ptr::null()) }; + assert!(!location.is_null()); + assert_eq!(unsafe { CStr::from_ptr(location) }.to_bytes(), b""); + + let abbreviations = elephc_tz_abbreviations(); + assert!(!abbreviations.is_null()); + assert!(!unsafe { CStr::from_ptr(abbreviations) }.to_bytes().is_empty()); + } + /// Location serializes the four tab-separated fields, including the special /// `UTC` values. #[test] @@ -218,4 +322,95 @@ mod tests { assert!(a_line.contains(":NULL"), "null id must use the NULL marker: {a_line}"); assert!(!a_line.ends_with(':'), "no trailing empty field: {a_line}"); } + + /// Decodes an `elephc_tz_offset` packed `i64` back into `(offset, isdst)`, the + /// way a caller (the windows runtime bridge) is expected to: mirrors the + /// scheme documented on `elephc_tz_offset` itself. + fn unpack(packed: i64) -> (i64, bool) { + let isdst = packed & 1; + ((packed - isdst) >> 1, isdst != 0) + } + + /// `elephc_tz_offset` round-trips through a real NUL-terminated C string: a + /// winter Europe/Paris instant packs to `(3600, false)`. + #[test] + fn elephc_tz_offset_resolves_known_zone() { + let name = CString::new("Europe/Paris").unwrap(); + let packed = unsafe { elephc_tz_offset(name.as_ptr(), 1_705_320_000) }; + assert_eq!(unpack(packed), (3600, false)); + } + + /// A summer Europe/Paris instant packs to `(7200, true)` (DST/CEST). + #[test] + fn elephc_tz_offset_resolves_dst() { + let name = CString::new("Europe/Paris").unwrap(); + let packed = unsafe { elephc_tz_offset(name.as_ptr(), 1_721_044_800) }; + assert_eq!(unpack(packed), (7200, true)); + } + + /// The abbreviation bridge returns transition-specific stable C strings. + #[test] + fn elephc_tz_abbreviation_resolves_winter_summer_and_utc() { + let paris = CString::new("Europe/Paris").unwrap(); + let winter = unsafe { elephc_tz_abbreviation(paris.as_ptr(), 1_705_320_000) }; + assert_eq!(unsafe { CStr::from_ptr(winter) }.to_bytes(), b"CET"); + let summer = unsafe { elephc_tz_abbreviation(paris.as_ptr(), 1_721_044_800) }; + assert_eq!(unsafe { CStr::from_ptr(summer) }.to_bytes(), b"CEST"); + let utc = CString::new("UTC").unwrap(); + let utc_abbreviation = unsafe { elephc_tz_abbreviation(utc.as_ptr(), 0) }; + assert_eq!(unsafe { CStr::from_ptr(utc_abbreviation) }.to_bytes(), b"UTC"); + } + + /// An unknown zone name returns the `i64::MIN` sentinel, telling the caller + /// to fall back to its own (non-bridge) offset resolution. + #[test] + fn elephc_tz_offset_unknown_zone_is_sentinel() { + let name = CString::new("Not/AZone").unwrap(); + let packed = unsafe { elephc_tz_offset(name.as_ptr(), 0) }; + assert_eq!(packed, i64::MIN); + } + + /// A null name pointer also resolves to the sentinel (empty zone name is + /// unknown), rather than panicking. + #[test] + fn elephc_tz_offset_null_name_is_sentinel() { + let packed = unsafe { elephc_tz_offset(std::ptr::null(), 0) }; + assert_eq!(packed, i64::MIN); + } + + /// A panic in a string export is contained and returned as the valid empty + /// C-string sentinel rather than unwinding across the ABI boundary. + #[test] + fn string_export_panic_is_empty_sentinel() { + let cell = Box::leak(Box::new(Mutex::new(CString::default()))); + let ptr = catch_string_export(cell, || panic!("injected tz string failure")); + let value = unsafe { CStr::from_ptr(ptr) }; + assert_eq!(value.to_bytes(), b""); + } + + /// A panic in the scalar offset export is contained and returned as the + /// documented impossible-offset sentinel. + #[test] + fn scalar_export_panic_is_min_sentinel() { + let value = catch_scalar_export(|| panic!("injected tz offset failure"), i64::MIN); + assert_eq!(value, i64::MIN); + } + + /// A panic while holding a result-buffer mutex poisons it, after which stash + /// recovers the inner value and still publishes a valid replacement string. + #[test] + fn stash_recovers_poisoned_buffer() { + let cell: &'static Mutex = + Box::leak(Box::new(Mutex::new(CString::default()))); + let cell_for_panic = cell; + let _ = std::thread::spawn(move || { + let _guard = cell_for_panic.lock().unwrap(); + panic!("inject buffer poison"); + }) + .join(); + + let ptr = stash(cell, "recovered".to_string()); + let value = unsafe { CStr::from_ptr(ptr) }; + assert_eq!(value.to_bytes(), b"recovered"); + } } diff --git a/crates/elephc-tz/src/lib.rs b/crates/elephc-tz/src/lib.rs index 0db2f3b948..a5ca9b23e4 100644 --- a/crates/elephc-tz/src/lib.rs +++ b/crates/elephc-tz/src/lib.rs @@ -206,6 +206,29 @@ pub fn zone_location(name: &str) -> Option { }) } +/// Resolves `name`'s UTC offset (seconds) and DST flag at Unix timestamp `ts`, for +/// the windows-only local-time bridge (`elephc_tz_offset` in the `abi` module). +/// Binary-searches [`zone_transitions`] for the last transition with +/// `transition.ts <= ts` (rows are ascending by construction — see +/// `data/generate.php`); before the zone's first transition, timelib/PHP falls +/// back to that first row's data (the synthetic LMT/earliest-known-offset row, +/// e.g. Europe/Paris row 0 at `i64::MIN`), matching `getTransitions()`'s own row 0 +/// semantics. Returns `None` for an unknown zone or a false-zone (no transition +/// data), mirroring `zone_transitions` — callers fall back to their own +/// (non-bridge) offset resolution in that case. +/// +/// The abbreviation is returned as an owned `String` (not `&'static str`, unlike +/// [`TzTransition::abbr`]'s field which borrows from the embedded static table) +/// purely so this function can build its result independently of the borrowed +/// `Vec` it searches, which is dropped on return; the offset/DST +/// bridge caller does not currently consume it. +pub fn zone_offset_at(name: &str, ts: i64) -> Option<(i32, bool, String)> { + let rows = zone_transitions(name)?; + let idx = rows.partition_point(|row| row.ts <= ts); + let row = if idx == 0 { rows.first()? } else { &rows[idx - 1] }; + Some((row.offset as i32, row.isdst, row.abbr.clone())) +} + /// Returns the full `listAbbreviations()` table in PHP's exact key and row order, /// parsed once from the embedded data. An empty id field decodes to `None` (PHP's /// null `timezone_id`); PHP never emits an empty-string id. @@ -355,4 +378,52 @@ mod tests { assert_eq!(LOCATIONS.lines().count(), 598); assert_eq!(TRANSITIONS.lines().count(), 598); } + + /// Europe/Paris resolves to standard time (CET, +3600, no DST) at a known + /// winter instant (2024-01-15 12:00:00 UTC). + #[test] + fn zone_offset_at_paris_winter() { + let (offset, isdst, abbr) = zone_offset_at("Europe/Paris", 1_705_320_000) + .expect("Europe/Paris resolves in winter"); + assert_eq!(offset, 3600); + assert!(!isdst); + assert_eq!(abbr, "CET"); + } + + /// Europe/Paris resolves to daylight time (CEST, +7200, DST) at a known + /// summer instant (2024-07-15 12:00:00 UTC). + #[test] + fn zone_offset_at_paris_summer() { + let (offset, isdst, abbr) = zone_offset_at("Europe/Paris", 1_721_044_800) + .expect("Europe/Paris resolves in summer"); + assert_eq!(offset, 7200); + assert!(isdst); + assert_eq!(abbr, "CEST"); + } + + /// Before Europe/Paris's first real transition, resolution falls back to the + /// synthetic LMT row 0 (561-second offset), matching `getTransitions()`'s own + /// row 0 semantics — exercised with a deeply negative pre-1900 timestamp. + #[test] + fn zone_offset_at_pre_first_transition_uses_lmt_row() { + let (offset, isdst, abbr) = + zone_offset_at("Europe/Paris", -2_840_140_800).expect("resolves before row 1 (1880)"); + assert_eq!(offset, 561); + assert!(!isdst); + assert_eq!(abbr, "LMT"); + } + + /// UTC always resolves to a zero offset, unaffected by the query timestamp. + #[test] + fn zone_offset_at_utc_is_always_zero() { + let (offset, isdst, _) = zone_offset_at("UTC", 0).expect("UTC resolves"); + assert_eq!(offset, 0); + assert!(!isdst); + } + + /// An unknown zone name resolves to `None`, mirroring `zone_transitions`. + #[test] + fn zone_offset_at_unknown_zone_is_none() { + assert!(zone_offset_at("Not/AZone", 0).is_none()); + } } diff --git a/crates/elephc-web/Cargo.toml b/crates/elephc-web/Cargo.toml index 55299fdf14..b86aca6b3c 100644 --- a/crates/elephc-web/Cargo.toml +++ b/crates/elephc-web/Cargo.toml @@ -2,7 +2,7 @@ name = "elephc-web" version = "0.1.0" edition = "2021" -description = "Prefork HTTP server staticlib for the elephc --web flag (hyper + tokio)." +description = "Cross-platform HTTP server staticlib for the elephc --web flag (hyper + tokio)." license = "MIT" publish = false @@ -27,3 +27,9 @@ libc = "0.2" # flate2: optional gzip response compression (--gzip). Already in the workspace # dependency tree, so this adds no new external crate. flate2 = "1" + +[target.'cfg(windows)'.dependencies] +# Portable console Ctrl-C handling for the single-process Windows event loop. +ctrlc = "3" +# Native whole-file session locking and reparse-point-safe opens. +windows-sys = { version = "0.61", features = ["Win32_Storage_FileSystem", "Win32_System_IO"] } diff --git a/crates/elephc-web/src/lib.rs b/crates/elephc-web/src/lib.rs index 911179b05f..1c1bee2976 100644 --- a/crates/elephc-web/src/lib.rs +++ b/crates/elephc-web/src/lib.rs @@ -1,5 +1,5 @@ //! Purpose: -//! C-ABI surface for the elephc `--web` prefork HTTP server bridge. Exposes the +//! C-ABI surface for the elephc `--web` HTTP server bridge. Exposes the //! server entry point and (in later phases) request/response marshaling under //! `#[no_mangle] extern "C"` symbols the compiled PHP program calls/links. //! @@ -10,8 +10,9 @@ //! - Tests: directly through the `rlib` crate type. //! //! Key details: -//! - One process per prefork worker means no shared-thread state: per-worker -//! request/response data lives in plain process statics, not behind a mutex. +//! - Unix uses one process per prefork worker; Windows uses one process and one +//! PHP execution thread. Request/response data therefore lives in plain +//! process statics, not behind a mutex. mod multipart; mod request_state; diff --git a/crates/elephc-web/src/request_state.rs b/crates/elephc-web/src/request_state.rs index d00c653ab1..560747681b 100644 --- a/crates/elephc-web/src/request_state.rs +++ b/crates/elephc-web/src/request_state.rs @@ -24,6 +24,7 @@ use std::ffi::{c_char, CString}; +#[cfg(not(test))] extern "C" { /// Per-request output-capture flag defined in the compiled program's runtime /// `.comm` storage (`elephc_web_capture`). Non-zero routes the runtime's @@ -34,6 +35,11 @@ extern "C" { static mut elephc_web_capture: u8; } +/// Test-local stand-in for the generated runtime's capture flag. +#[cfg(test)] +#[no_mangle] +static mut elephc_web_capture: u8 = 0; + /// Process-static per-worker response body. Bytes echoed by the PHP handler land /// here while capture is enabled; the server scaffold flushes it to the client /// (currently stdout) once the handler returns. @@ -245,6 +251,7 @@ pub(crate) fn set_request( body: Vec, meta: RequestMeta, ) { + /// Converts request text to a NUL-free C string for the runtime request state. fn cstr(s: &str) -> CString { CString::new(s.replace('\0', "")).unwrap_or_default() } diff --git a/crates/elephc-web/src/server.rs b/crates/elephc-web/src/server.rs index 410718f00d..a4d5e379ea 100644 --- a/crates/elephc-web/src/server.rs +++ b/crates/elephc-web/src/server.rs @@ -1,17 +1,20 @@ //! Purpose: -//! The `--web` server entry point: parse the binary's runtime args, prefork N -//! worker processes, and supervise them. Each worker serves HTTP independently. +//! The `--web` server entry point: parse the binary's runtime args and dispatch +//! to the Unix prefork supervisor or the Windows single-process event loop. //! //! Called from: //! - The compiled `--web` binary's process entry (tail-call to elephc_web_run). //! //! Key details: -//! - fork() happens BEFORE any tokio runtime is created (tokio does not survive -//! fork); each child builds its own current-thread runtime in worker::serve. +//! - On Unix, fork() happens before any tokio runtime is created; each child +//! builds its own current-thread runtime in worker::serve. +//! - On Windows, one current-thread event loop serializes PHP execution while +//! allowing concurrent connection I/O; no Unix process or signal API is linked. //! - --listen host:port is required; without it the process errors and exits. use std::ffi::{c_char, CStr}; use std::sync::atomic::{AtomicBool, Ordering}; +#[cfg(unix)] use std::time::{Duration, Instant}; use crate::worker::{self, WorkerConfig}; @@ -20,15 +23,15 @@ use crate::worker::{self, WorkerConfig}; const HELP: &str = "\ Usage: --listen HOST:PORT [options] -A standalone prefork HTTP server compiled from PHP by `elephc --web`. +A standalone HTTP server compiled from PHP by `elephc --web`. Options: --listen HOST:PORT Address to bind (required), e.g. 127.0.0.1:8080 - --workers N Number of prefork worker processes (default: CPU count) + --workers N Number of prefork workers on Unix (default: CPU count; Windows uses one event loop) --max-body-size BYTES Max request body in bytes; 0 = unlimited (default: 8388608) - --max-requests N Recycle a worker after N requests; 0 = never (default: 0) + --max-requests N Recycle a Unix worker or stop Windows after N requests; 0 = never (default: 0) --access-log Log one line per request to stderr - --max-execution-time N Kill (and respawn) a worker whose handler runs > N seconds; 0 = no limit + --max-execution-time N Stop the server if a handler runs > N seconds; Unix respawns its worker; 0 = no limit --gzip Compress responses when the client sends Accept-Encoding: gzip --help Show this help and exit --version Show the server version and exit"; @@ -36,15 +39,18 @@ Options: /// A worker that dies within this window of being spawned counts as a crash-on- /// startup; too many in a row (e.g. a bind failure or a handler that crashes on /// every request) abort the master instead of fork-looping forever. +#[cfg(unix)] const FAST_DEATH: Duration = Duration::from_millis(1000); /// Consecutive fast worker deaths tolerated before the master gives up. +#[cfg(unix)] const MAX_FAST_DEATHS: u32 = 10; /// Set by the SIGINT/SIGTERM handler so the master supervision loop can break and /// shut workers down cleanly. Async-signal-safe: the handler only stores to it. -static SHUTDOWN: AtomicBool = AtomicBool::new(false); +pub(crate) static SHUTDOWN: AtomicBool = AtomicBool::new(false); /// Async-signal-safe SIGINT/SIGTERM handler: records the shutdown request only. +#[cfg(unix)] extern "C" fn handle_shutdown_signal(_sig: libc::c_int) { SHUTDOWN.store(true, Ordering::SeqCst); } @@ -52,6 +58,7 @@ extern "C" fn handle_shutdown_signal(_sig: libc::c_int) { /// Installs `handle_shutdown_signal` for SIGINT and SIGTERM WITHOUT `SA_RESTART`, /// so a signal interrupts the master's blocking `waitpid` (returns EINTR) instead /// of silently restarting it. +#[cfg(unix)] fn install_signal_handlers() { unsafe { let mut sa: libc::sigaction = std::mem::zeroed(); @@ -67,6 +74,7 @@ fn install_signal_handlers() { /// this so it does NOT inherit the master's catch-and-flag handler — otherwise a /// worker would catch the master's forwarded SIGTERM and never terminate, hanging /// the master's reap. With SIG_DFL a forwarded SIGTERM terminates the worker. +#[cfg(unix)] fn reset_signal_handlers_to_default() { unsafe { let mut sa: libc::sigaction = std::mem::zeroed(); @@ -189,6 +197,7 @@ fn default_workers() -> usize { /// (then exits with `worker::RECYCLE_EXIT_CODE`), returning the child pid in the /// master. The child restores default signal disposition and never returns. A /// fork failure aborts the whole process. Used for both initial spawn and respawn. +#[cfg(unix)] fn spawn_worker(listen: &str, handler: extern "C" fn(), cfg: WorkerConfig) -> libc::pid_t { match unsafe { libc::fork() } { -1 => { @@ -197,7 +206,7 @@ fn spawn_worker(listen: &str, handler: extern "C" fn(), cfg: WorkerConfig) -> li } 0 => { reset_signal_handlers_to_default(); - worker::serve(listen, handler, cfg); + worker::serve(listen, handler, cfg, None); // serve() only returns when the worker stopped accepting on purpose // after serving its --max-requests quota; exit with the recycle code // so the reaper skips the crash-loop accounting for this death. @@ -213,6 +222,7 @@ fn spawn_worker(listen: &str, handler: extern "C" fn(), cfg: WorkerConfig) -> li /// the fast-death accounting. Without this distinction, sustained traffic with /// a small `--max-requests` recycles workers faster than `FAST_DEATH` and the /// master mistakes the healthy recycle churn for a startup crash loop. +#[cfg(unix)] fn is_planned_recycle(status: libc::c_int) -> bool { libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == worker::RECYCLE_EXIT_CODE } @@ -232,6 +242,14 @@ pub extern "C" fn elephc_web_run( ParsedArgs::Run(a) => a, ParsedArgs::Exit(code) => return code, }; + SHUTDOWN.store(false, Ordering::SeqCst); + run_server(args, handler) +} + +/// Runs the Unix prefork supervisor while keeping every Unix-only API outside +/// Windows builds. +#[cfg(unix)] +fn run_server(args: ServerArgs, handler: extern "C" fn()) -> i32 { install_signal_handlers(); // Fork workers BEFORE creating any tokio runtime. Track each worker's spawn // time so a crash-on-startup loop (e.g. a failed bind) can be detected. @@ -311,7 +329,45 @@ pub extern "C" fn elephc_web_run( 0 } +/// Runs the Windows server in one process and one PHP execution thread. +/// +/// Hyper still multiplexes connection I/O through local tasks, but the compiled +/// PHP handler and its process-global request state never execute concurrently. +/// `--max-requests` stops the Windows process cleanly so an external service +/// manager can restart with fresh state; Ctrl-C sets the shared shutdown flag +/// and lets the accept poll return cleanly. +#[cfg(windows)] +fn run_server(args: ServerArgs, handler: extern "C" fn()) -> i32 { + if let Err(error) = ctrlc::set_handler(|| SHUTDOWN.store(true, Ordering::SeqCst)) { + eprintln!("elephc-web: failed to install Ctrl-C handler: {error}"); + return 1; + } + if args.workers != 1 { + eprintln!( + "elephc-web: Windows uses one event-loop worker; ignoring --workers {}", + args.workers + ); + } + eprintln!( + "elephc-web: listening on http://{} (Windows event-loop worker)", + args.listen + ); + worker::serve(&args.listen, handler, args.worker_config(), Some(&SHUTDOWN)); + 0 +} + +/// Names the process model compiled for the current platform for diagnostics +/// and cfg-focused unit coverage. #[cfg(test)] +fn worker_model_name() -> &'static str { + if cfg!(windows) { + "windows-event-loop" + } else { + "unix-prefork" + } +} + +#[cfg(all(test, unix))] mod tests { use super::*; @@ -350,3 +406,18 @@ mod tests { assert!(!is_planned_recycle(libc::SIGTERM)); } } + +#[cfg(test)] +mod platform_tests { + use super::*; + + /// Verifies cfg selection exposes exactly one documented server process model. + #[test] + fn worker_model_matches_target_family() { + if cfg!(windows) { + assert_eq!(worker_model_name(), "windows-event-loop"); + } else { + assert_eq!(worker_model_name(), "unix-prefork"); + } + } +} diff --git a/crates/elephc-web/src/session/file_io.rs b/crates/elephc-web/src/session/file_io.rs index 3c60058084..2a7e1e06c5 100644 --- a/crates/elephc-web/src/session/file_io.rs +++ b/crates/elephc-web/src/session/file_io.rs @@ -1,5 +1,5 @@ //! Purpose: -//! Owns session file I/O for `--web` mode: flock'd read/write/destroy/abort, +//! Owns session file I/O for `--web` mode: locked read/write/destroy/abort, //! the `session_reset`/`lazy_write` snapshot and mtime-touch primitives, a //! filesystem existence check for `session.use_strict_mode`, and mtime-based //! garbage collection of `sess_` files under the configured save path. @@ -12,16 +12,18 @@ //! //! Key details: //! - One process per prefork worker, single-threaded, so the held-fd state -//! (`state::SESSION_FD`) is race-free across a request's read → write/destroy/ -//! abort/touch sequence. +//! (`SESSION_FILE`, exposed to sibling state as the `SESSION_FD` sentinel) is +//! race-free across a request's read → write/destroy/abort/touch sequence. //! - Save paths follow php-src's `[depth;[mode;]]path` grammar; every operation //! shares the same ID-derived path calculation and no-follow/owner checks. //! - BUG-3: `write` now writes in place on the held fd/inode as its *primary* -//! path (`ftruncate`+`pwrite`+`fsync`), since the `flock` is held on that -//! inode — a temp+rename would swap in a new inode the lock no longer -//! covers, breaking concurrent-writer serialization. Temp+rename remains the -//! fallback for the no-fd-held branch (e.g. `session_regenerate_id`'s fresh -//! id, which has no concurrent reader to serialize against). +//! path (truncate+seek+write+sync), since the advisory lock is held on that +//! file — a temp+rename would swap in a new file the lock no longer covers, +//! breaking concurrent-writer serialization. Temp+rename remains the +//! fallback for the no-file-held branch (e.g. `session_regenerate_id`'s +//! fresh id, which has no concurrent reader to serialize against). +//! - Unix uses `flock`; Windows uses an exclusive whole-file `LockFileEx` +//! range and retains the `File` so a pointer-sized HANDLE is never truncated. //! - §2.5 `touch` (lazy_write) MUST release the held lock exactly like //! `write` does — leaving it held would self-deadlock the next `read` //! (BUG-1's mechanism). @@ -35,24 +37,37 @@ use super::state::{ RET_STRING, SESSION_FD, SESSION_ID, SESSION_SNAPSHOT, }; use std::ffi::c_char; -use std::fs::{self, OpenOptions}; -use std::io::{Read, Write}; +use std::fs::{self, File, FileTimes, OpenOptions}; +use std::io::{Read, Seek, SeekFrom, Write}; +#[cfg(unix)] use std::os::unix::fs::{MetadataExt, OpenOptionsExt}; -use std::os::unix::io::{AsRawFd, IntoRawFd}; +#[cfg(unix)] +use std::os::unix::io::AsRawFd; +#[cfg(windows)] +use std::os::windows::fs::OpenOptionsExt; +#[cfg(windows)] +use std::os::windows::io::AsRawHandle; use std::path::PathBuf; /// Result of the most recent binary-safe files-handler read operation. static mut LAST_READ_OK: i64 = 1; -/// Releases the held session-file lock (`flock(LOCK_UN)`) and closes the file -/// descriptor. No-op when no fd is held. Single-threaded per worker. +/// Session file kept open and exclusively locked between read and completion. +/// +/// `SESSION_FD` remains the small cross-module held/not-held sentinel, while +/// this owner retains the actual Rust file so Windows HANDLE values are never +/// truncated into an `i32`. +static mut SESSION_FILE: Option = None; + +/// Releases and closes the held session file. No-op when none is held. +/// Single-threaded per worker. pub(super) unsafe fn release_lock() { - let fd = *core::ptr::addr_of!(SESSION_FD); - if fd >= 0 { - libc::flock(fd, libc::LOCK_UN); - libc::close(fd); - core::ptr::write(core::ptr::addr_of_mut!(SESSION_FD), -1); + let file = core::ptr::replace(core::ptr::addr_of_mut!(SESSION_FILE), None); + if let Some(file) = file { + unlock_file(&file); + drop(file); } + core::ptr::write(core::ptr::addr_of_mut!(SESSION_FD), -1); } /// Builds the full path for a session file: `/sess_`. @@ -127,27 +142,45 @@ pub(super) fn open_session_file( path: &std::path::Path, mode: u32, ) -> std::io::Result { - let file = OpenOptions::new() - .read(true) - .write(true) - .create(true) + #[cfg(windows)] + let _ = mode; + let mut options = OpenOptions::new(); + options.read(true).write(true).create(true); + #[cfg(unix)] + options .mode(mode) - .custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC) - .open(path)?; - let metadata = file.metadata()?; - let uid = unsafe { libc::getuid() }; - let euid = unsafe { libc::geteuid() }; - if uid != 0 && metadata.uid() != 0 && metadata.uid() != uid && metadata.uid() != euid { + .custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC); + #[cfg(windows)] + options.custom_flags( + windows_sys::Win32::Storage::FileSystem::FILE_FLAG_OPEN_REPARSE_POINT, + ); + let file = options.open(path)?; + #[cfg(unix)] + { + let metadata = file.metadata()?; + let uid = unsafe { libc::getuid() }; + let euid = unsafe { libc::geteuid() }; + if uid != 0 && metadata.uid() != 0 && metadata.uid() != uid && metadata.uid() != euid { + return Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "session file is owned by another user", + )); + } + } + #[cfg(windows)] + if file.metadata()?.file_type().is_symlink() { return Err(std::io::Error::new( std::io::ErrorKind::PermissionDenied, - "session file is owned by another user", + "session file is a reparse-point symbolic link", )); } Ok(file) } /// Acquires an exclusive advisory lock, retrying when interrupted by a signal. -pub(super) fn lock_exclusive(fd: i32) -> bool { +#[cfg(unix)] +pub(super) fn lock_exclusive(file: &File) -> bool { + let fd = file.as_raw_fd(); loop { let result = unsafe { libc::flock(fd, libc::LOCK_EX) }; if result == 0 { @@ -159,9 +192,82 @@ pub(super) fn lock_exclusive(fd: i32) -> bool { } } -/// Reads the session file for `id` under `save_path`. Opens with -/// `O_RDWR | O_CREAT`, acquires `flock(LOCK_EX)`, reads the content, and stores -/// the fd (held for later `session_write`/`session_destroy`/`session_abort`). +/// Acquires an exclusive Windows byte-range lock covering the entire file. +#[cfg(windows)] +pub(super) fn lock_exclusive(file: &File) -> bool { + let mut overlapped = windows_sys::Win32::System::IO::OVERLAPPED::default(); + unsafe { + windows_sys::Win32::Storage::FileSystem::LockFileEx( + file.as_raw_handle(), + windows_sys::Win32::Storage::FileSystem::LOCKFILE_EXCLUSIVE_LOCK, + 0, + u32::MAX, + u32::MAX, + &mut overlapped, + ) != 0 + } +} + +/// Releases a Unix advisory lock held by `file`. +#[cfg(unix)] +fn unlock_file(file: &File) { + unsafe { + libc::flock(file.as_raw_fd(), libc::LOCK_UN); + } +} + +/// Releases the Windows byte-range lock held by `file`. +#[cfg(windows)] +fn unlock_file(file: &File) { + let mut overlapped = windows_sys::Win32::System::IO::OVERLAPPED::default(); + unsafe { + windows_sys::Win32::Storage::FileSystem::UnlockFileEx( + file.as_raw_handle(), + 0, + u32::MAX, + u32::MAX, + &mut overlapped, + ); + } +} + +/// Replaces a locked session file's contents in place and flushes them. +pub(super) fn write_file_in_place(file: &mut File, data: &[u8]) -> std::io::Result<()> { + file.set_len(0)?; + file.seek(SeekFrom::Start(0))?; + file.write_all(data)?; + file.sync_all() +} + +/// Opens a no-follow temporary session file for the atomic replacement path. +fn open_temporary_session_file(path: &std::path::Path, mode: u32) -> std::io::Result { + #[cfg(windows)] + let _ = mode; + let mut options = OpenOptions::new(); + options.write(true).create(true).truncate(true); + #[cfg(unix)] + options + .mode(mode) + .custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC); + #[cfg(windows)] + options.custom_flags( + windows_sys::Win32::Storage::FileSystem::FILE_FLAG_OPEN_REPARSE_POINT, + ); + options.open(path) +} + +/// Updates a session file's access and modification timestamps to now. +fn touch_session_file(path: &std::path::Path) -> std::io::Result<()> { + let now = std::time::SystemTime::now(); + OpenOptions::new() + .write(true) + .open(path)? + .set_times(FileTimes::new().set_accessed(now).set_modified(now)) +} + +/// Reads the session file for `id` under `save_path`. Opens read/write with +/// creation, acquires the platform lock, reads the content, and retains the +/// file for later `session_write`/`session_destroy`/`session_abort`. /// When `read_and_close=1`, releases the lock and closes the fd immediately /// after reading (no write will happen at handler end). Publishes the exact file /// bytes in the shared pointer/length transfer buffer and returns its pointer. @@ -189,20 +295,20 @@ pub unsafe extern "C" fn elephc_web_session_read_bytes( }; // Open with O_RDWR | O_CREAT, mode 0600. - let file = match open_session_file(&path, config.mode) { + let mut file = match open_session_file(&path, config.mode) { Ok(f) => f, Err(_) => return publish_bytes(&[]), }; - let fd = file.as_raw_fd(); // Acquire exclusive lock (blocks until the lock is available). - if !lock_exclusive(fd) { + if !lock_exclusive(&file) { return publish_bytes(&[]); } // Read the full content. let mut data = Vec::new(); - if (&file).read_to_end(&mut data).is_err() { + if file.read_to_end(&mut data).is_err() { + unlock_file(&file); return publish_bytes(&[]); } @@ -211,10 +317,10 @@ pub unsafe extern "C" fn elephc_web_session_read_bytes( drop(file); // fd is closed by drop; flock is released on close. } else { - // Hold the fd open with the lock for later write/destroy/abort. - // Convert the File into a raw fd we own (leak the File wrapper). - let raw_fd = file.into_raw_fd(); - core::ptr::write(core::ptr::addr_of_mut!(SESSION_FD), raw_fd); + // Retain the Rust file and lock for later write/destroy/abort. The + // numeric state is only a held/not-held sentinel on Windows. + core::ptr::write(core::ptr::addr_of_mut!(SESSION_FILE), Some(file)); + core::ptr::write(core::ptr::addr_of_mut!(SESSION_FD), 0); } // Store the snapshot for session_reset/session_abort. @@ -285,29 +391,12 @@ pub unsafe extern "C" fn elephc_web_session_write_bytes( }; let data = input_bytes(data_ptr, data_len); - let fd = *core::ptr::addr_of!(SESSION_FD); - if fd >= 0 { + if *core::ptr::addr_of!(SESSION_FD) >= 0 { // Primary path (BUG-3): in-place write on the held fd/inode. - let mut offset = 0usize; - let mut ok = libc::ftruncate(fd, 0) == 0; - while ok && offset < data.len() { - let wrote = libc::pwrite( - fd, - data[offset..].as_ptr() as *const _, - data.len() - offset, - offset as libc::off_t, - ); - if wrote > 0 { - offset += wrote as usize; - } else if wrote < 0 - && std::io::Error::last_os_error().kind() == std::io::ErrorKind::Interrupted - { - continue; - } else { - ok = false; - } - } - ok = ok && libc::fsync(fd) == 0; + let file = &mut *core::ptr::addr_of_mut!(SESSION_FILE); + let ok = file + .as_mut() + .is_some_and(|held| write_file_in_place(held, data).is_ok()); release_lock(); return i64::from(ok); } @@ -320,17 +409,10 @@ pub unsafe extern "C" fn elephc_web_session_write_bytes( let result = (|| -> std::io::Result<()> { { - let mut tmp = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .mode(config.mode) - .custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC) - .open(&tmp_path)?; + let mut tmp = open_temporary_session_file(&tmp_path, config.mode)?; tmp.write_all(data)?; // Ensure the data hits disk before the rename. - let tmp_fd = tmp.as_raw_fd(); - let _ = libc::fsync(tmp_fd); + tmp.sync_all()?; } fs::rename(&tmp_path, &path)?; Ok(()) @@ -481,11 +563,8 @@ pub unsafe extern "C" fn elephc_web_session_touch( release_lock(); return 0; }; - if let Ok(path_cstr) = std::ffi::CString::new(path.to_string_lossy().into_owned()) { - // A null `times` pointer sets both atime and mtime to "now". - if libc::utimes(path_cstr.as_ptr(), std::ptr::null()) == 0 { - ok = 1; - } + if touch_session_file(&path).is_ok() { + ok = 1; } } release_lock(); @@ -662,7 +741,10 @@ mod tests { payload ); let metadata = fs::metadata(dir.join("b/i/sess_binaryid")).unwrap(); + #[cfg(unix)] assert_eq!(metadata.mode() & 0o7777, 0o640); + #[cfg(windows)] + assert!(metadata.is_file()); let _ = fs::remove_dir_all(dir); } } @@ -674,6 +756,22 @@ mod tests { u64::from_le_bytes(bytes) } + /// Sets a fixture file's access and modification times to a Unix timestamp. + fn set_file_timestamp(path: &std::path::Path, seconds: i64) { + let timestamp = std::time::UNIX_EPOCH + + std::time::Duration::from_secs(u64::try_from(seconds).unwrap_or(0)); + std::fs::OpenOptions::new() + .write(true) + .open(path) + .unwrap() + .set_times( + std::fs::FileTimes::new() + .set_accessed(timestamp) + .set_modified(timestamp), + ) + .unwrap(); + } + /// Verifies session read/write/destroy round-trip with file locking. #[test] fn session_file_read_write_round_trip() { @@ -807,25 +905,13 @@ mod tests { let old_data = std::ffi::CString::new(b"x|i:2;".to_vec()).unwrap(); elephc_web_session_write(old_ptr, sp_ptr, old_data.as_ptr()); let old_path = session_file_path(&tmp, &old_id); - // Set mtime to 2 hours ago via libc::utimes (needs NUL-terminated path). + // Backdate the fixture through Rust's portable file-time API. let two_hours_ago = std::time::SystemTime::now() - std::time::Duration::from_secs(7200); let secs = two_hours_ago .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_secs() as i64) .unwrap_or(0); - let times = [ - libc::timeval { - tv_sec: secs, - tv_usec: 0, - }, - libc::timeval { - tv_sec: secs, - tv_usec: 0, - }, - ]; - let path_cstr = - std::ffi::CString::new(old_path.to_string_lossy().into_owned()).unwrap_or_default(); - let _ = libc::utimes(path_cstr.as_ptr(), times.as_ptr()); + set_file_timestamp(&old_path, secs); // GC with maxlifetime=3600 (1 hour) should delete the old file only. let deleted = elephc_web_session_gc(sp_ptr, 3600); @@ -861,12 +947,7 @@ mod tests { .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_secs() as i64; - let times = [ - libc::timeval { tv_sec: seconds, tv_usec: 0 }, - libc::timeval { tv_sec: seconds, tv_usec: 0 }, - ]; - let path_c = std::ffi::CString::new(path.to_string_lossy().as_bytes()).unwrap(); - libc::utimes(path_c.as_ptr(), times.as_ptr()); + set_file_timestamp(&path, seconds); assert_eq!(elephc_web_session_gc(configured.as_ptr(), 3600), 1); assert!(!path.exists()); let _ = fs::remove_dir_all(base); @@ -998,19 +1079,7 @@ mod tests { .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_secs() as i64) .unwrap_or(0); - let times = [ - libc::timeval { - tv_sec: old_secs, - tv_usec: 0, - }, - libc::timeval { - tv_sec: old_secs, - tv_usec: 0, - }, - ]; - let path_cstr = - std::ffi::CString::new(path.to_string_lossy().into_owned()).unwrap_or_default(); - let _ = libc::utimes(path_cstr.as_ptr(), times.as_ptr()); + set_file_timestamp(&path, old_secs); // Hold the lock via read, then touch instead of write. elephc_web_session_read(id_ptr, sp_ptr, 0); @@ -1102,19 +1171,7 @@ mod tests { .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_secs() as i64) .unwrap_or(0); - let times = [ - libc::timeval { - tv_sec: old_secs, - tv_usec: 0, - }, - libc::timeval { - tv_sec: old_secs, - tv_usec: 0, - }, - ]; - let path_cstr = std::ffi::CString::new(active_path.to_string_lossy().into_owned()) - .unwrap_or_default(); - let _ = libc::utimes(path_cstr.as_ptr(), times.as_ptr()); + set_file_timestamp(&active_path, old_secs); // Mark this ID as the active session (mirrors what session_start // does via elephc_web_session_set_id). diff --git a/crates/elephc-web/src/session/state.rs b/crates/elephc-web/src/session/state.rs index 93f2f52bb6..6870665ebb 100644 --- a/crates/elephc-web/src/session/state.rs +++ b/crates/elephc-web/src/session/state.rs @@ -62,8 +62,9 @@ pub(super) static mut COOKIE_HTTPONLY: bool = false; pub(super) static mut COOKIE_SAMESITE: Option = None; /// Data snapshot for `session_reset`/`session_abort` (the original file content). pub(super) static mut SESSION_SNAPSHOT: Vec = Vec::new(); -/// Held session-file descriptor (`-1` = none). Kept open with `flock(LOCK_EX)` -/// between `session_read` and `session_write`/`session_destroy`/`session_abort`. +/// Held session-file sentinel (`-1` = none, `0` = held). The actual `File` and +/// platform lock live in `file_io`, avoiding pointer-sized HANDLE truncation on +/// Windows while preserving the existing sibling-module state contract. pub(super) static mut SESSION_FD: i32 = -1; // ── Extended session configuration (v3 additions) ── diff --git a/crates/elephc-web/src/session/upload_progress.rs b/crates/elephc-web/src/session/upload_progress.rs index 912200c6e3..dd3fa11f0f 100644 --- a/crates/elephc-web/src/session/upload_progress.rs +++ b/crates/elephc-web/src/session/upload_progress.rs @@ -12,8 +12,8 @@ //! runs once the body is fully drained (before the PHP handler executes). //! //! Key details: -//! - Progress writes use independent `open -> flock(LOCK_EX) -> read -> modify -//! -> write -> close` cycles on `/sess_`; they NEVER touch the +//! - Progress writes use independent platform-locked `open -> read -> modify -> +//! write -> close` cycles on `/sess_`; they NEVER touch the //! handler's persistent `state::SESSION_FD`, so the short lock is never held //! across the whole upload. The handler runs after the drain, so all progress //! writes are flushed and unlocked before `session_start` locks the file. @@ -26,11 +26,11 @@ use std::ffi::CStr; use std::io::Read; -use std::os::unix::io::AsRawFd; use std::time::Instant; use super::file_io::{ configured_session_file_path, lock_exclusive, open_session_file, parse_save_path, + write_file_in_place, }; /// Per-file progress snapshot mirroring one entry of PHP's `files` sub-array. @@ -277,10 +277,10 @@ impl Tracker { self.rmw(|data| Some(remove_entry(data, full_key, self.handler))) } - /// Runs `edit` under an independent `open -> flock(LOCK_EX) -> read -> - /// truncate+write -> unlock -> close` cycle on the session file. Never uses - /// the handler's persistent fd. Closes every transient fd (no fd leak) and - /// releases the lock on every path. Returns true when the edit was written. + /// Runs `edit` under an independent platform-locked `open -> read -> + /// truncate+write -> close` cycle on the session file. Never uses the + /// handler's persistent file. Closes every transient file and releases the + /// lock on every path. Returns true when the edit was written. fn rmw(&self, edit: impl FnOnce(&[u8]) -> Option>) -> bool { let Some(sid) = self.sid.as_deref() else { return false; @@ -291,53 +291,22 @@ impl Tracker { let Some(path) = configured_session_file_path(&self.save_path, sid) else { return false; }; - let file = match open_session_file(&path, config.mode) { + let mut file = match open_session_file(&path, config.mode) { Ok(f) => f, Err(_) => return false, }; - let fd = file.as_raw_fd(); - // SAFETY: fd is valid for the lifetime of `file`; flock is advisory and - // serializes this transient writer against concurrent poll requests. - if !lock_exclusive(fd) { + if !lock_exclusive(&file) { return false; } let mut data = Vec::new(); - if (&file).read_to_end(&mut data).is_err() { + if file.read_to_end(&mut data).is_err() { return false; } let mut ok = false; if let Some(new_data) = edit(&data) { - // SAFETY: truncate then retry `pwrite` until the full buffer is on - // disk; interrupted and short writes are not success. - unsafe { - ok = libc::ftruncate(fd, 0) == 0; - let mut offset = 0usize; - while ok && offset < new_data.len() { - let wrote = libc::pwrite( - fd, - new_data[offset..].as_ptr() as *const _, - new_data.len() - offset, - offset as libc::off_t, - ); - if wrote > 0 { - offset += wrote as usize; - } else if wrote < 0 - && std::io::Error::last_os_error().kind() - == std::io::ErrorKind::Interrupted - { - continue; - } else { - ok = false; - } - } - ok = ok && libc::fsync(fd) == 0; - } - } - // SAFETY: release the advisory lock before the fd is closed by `drop`. - unsafe { - libc::flock(fd, libc::LOCK_UN); + ok = write_file_in_place(&mut file, &new_data).is_ok(); } - drop(file); // closes the fd + drop(file); // closes the file and releases its OS lock ok } } diff --git a/crates/elephc-web/src/worker.rs b/crates/elephc-web/src/worker.rs index ab0d1f0d98..411c1f0f3a 100644 --- a/crates/elephc-web/src/worker.rs +++ b/crates/elephc-web/src/worker.rs @@ -1,18 +1,18 @@ //! Purpose: -//! Per-worker HTTP serving: build a SO_REUSEPORT listening socket, run a tokio -//! current-thread runtime, and dispatch each request to the PHP handler. +//! Per-worker HTTP serving: build a listening socket, run a tokio current-thread +//! runtime, and dispatch each request to the PHP handler. //! //! Called from: //! - `crate::server::elephc_web_run` in each forked child process. //! //! Key details: //! - current-thread runtime + a blocking handler() call means PHP never runs on -//! two threads in one worker; concurrency comes from the N forked workers. -//! - SO_REUSEPORT lets every worker bind the same port; the kernel balances. +//! two threads in one worker; Unix concurrency also comes from prefork workers. +//! - SO_REUSEPORT is Unix-only. Windows uses one listener and multiplexes I/O. use std::convert::Infallible; use std::net::SocketAddr; -use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; use std::time::{Duration, Instant}; use http_body_util::{BodyExt, Full, Limited}; @@ -30,11 +30,12 @@ use crate::session::upload_progress; /// Pending-connection backlog for each worker's listening socket. const LISTEN_BACKLOG: i32 = 1024; -/// Builds a listening std::net::TcpListener with SO_REUSEPORT set, bound to `addr`. +/// Builds a nonblocking listening socket bound to `addr`. fn reuseport_listener(addr: SocketAddr) -> std::io::Result { let domain = if addr.is_ipv6() { Domain::IPV6 } else { Domain::IPV4 }; let sock = Socket::new(domain, Type::STREAM, Some(Protocol::TCP))?; sock.set_reuse_address(true)?; + #[cfg(unix)] sock.set_reuse_port(true)?; sock.set_nonblocking(true)?; sock.bind(&addr.into())?; @@ -52,6 +53,7 @@ static SERVED: AtomicUsize = AtomicUsize::new(0); /// under sustained traffic a worker can serve its whole quota in well under the /// master's fast-death window, and counting that as a crash-on-startup would /// shut the server down. Checked by `server::is_planned_recycle`. +#[cfg(unix)] pub(crate) const RECYCLE_EXIT_CODE: i32 = 86; /// Per-request handler time limit in seconds (`0` = none), read by `run_handler` @@ -61,6 +63,7 @@ static MAX_EXEC_SECS: AtomicU32 = AtomicU32::new(0); /// `SIGALRM` handler: a handler that ran past `--max-execution-time` is killed so /// the master respawns the worker (a runaway handler would otherwise pin the /// single worker thread forever). Async-signal-safe: only `write` + `_exit`. +#[cfg(unix)] extern "C" fn handle_exec_timeout(_sig: libc::c_int) { const MSG: &[u8] = b"elephc-web: handler exceeded --max-execution-time; recycling worker\n"; unsafe { @@ -70,6 +73,7 @@ extern "C" fn handle_exec_timeout(_sig: libc::c_int) { } /// Installs the `SIGALRM` execution-timeout handler in this worker. +#[cfg(unix)] fn install_exec_timeout_handler() { unsafe { let mut sa: libc::sigaction = std::mem::zeroed(); @@ -103,7 +107,12 @@ const GZIP_MIN_LEN: usize = 256; /// Serves HTTP on `listen` (host:port) in this worker process. Builds a /// current-thread tokio runtime and loops accepting connections, serving each /// with the PHP handler per `WorkerConfig`. -pub fn serve(listen: &str, handler: extern "C" fn(), cfg: WorkerConfig) { +pub fn serve( + listen: &str, + handler: extern "C" fn(), + cfg: WorkerConfig, + shutdown: Option<&'static AtomicBool>, +) { let WorkerConfig { max_body, max_requests, @@ -111,8 +120,10 @@ pub fn serve(listen: &str, handler: extern "C" fn(), cfg: WorkerConfig) { max_exec_secs, gzip, } = cfg; + SERVED.store(0, Ordering::Relaxed); if max_exec_secs > 0 { MAX_EXEC_SECS.store(max_exec_secs, Ordering::Relaxed); + #[cfg(unix)] install_exec_timeout_handler(); } let addr: SocketAddr = match listen.parse() { @@ -153,9 +164,16 @@ pub fn serve(listen: &str, handler: extern "C" fn(), cfg: WorkerConfig) { if max_requests > 0 && SERVED.load(Ordering::Relaxed) >= max_requests { break; } - let (stream, peer) = match listener.accept().await { - Ok(pair) => pair, - Err(_) => continue, + let accepted = tokio::time::timeout(Duration::from_millis(100), listener.accept()).await; + let (stream, peer) = match accepted { + Ok(Ok(pair)) => pair, + Ok(Err(_)) => continue, + Err(_) => { + if shutdown.is_some_and(|flag| flag.load(Ordering::SeqCst)) { + break; + } + continue; + } }; let io = TokioIo::new(stream); tokio::task::spawn_local(http1::Builder::new() @@ -331,13 +349,72 @@ fn run_handler(handler: extern "C" fn()) -> Vec { request_state::reset_response(); // Arm the execution-timeout watchdog around the blocking handler, if enabled. let secs = MAX_EXEC_SECS.load(Ordering::Relaxed); + run_handler_with_timeout(handler, secs); + SERVED.fetch_add(1, Ordering::Relaxed); + request_state::take_body() +} + +/// Runs the blocking handler under the Unix SIGALRM worker watchdog. +#[cfg(unix)] +fn run_handler_with_timeout(handler: extern "C" fn(), secs: u32) { if secs > 0 { - unsafe { libc::alarm(secs); } + unsafe { libc::alarm(secs) }; } handler(); if secs > 0 { - unsafe { libc::alarm(0); } + unsafe { libc::alarm(0) }; + } +} + +/// Runs the blocking handler with a Windows watchdog thread. +/// +/// A timeout terminates the server process, matching the Unix worker-fatal +/// contract without attempting unsafe forced thread cancellation while PHP may +/// hold process-global runtime state. +#[cfg(windows)] +fn run_handler_with_timeout(handler: extern "C" fn(), secs: u32) { + if secs == 0 { + handler(); + return; + } + let (done_tx, done_rx) = std::sync::mpsc::sync_channel::<()>(0); + let watchdog = std::thread::spawn(move || { + if done_rx + .recv_timeout(Duration::from_secs(secs.into())) + .is_err() + { + eprintln!("elephc-web: handler exceeded --max-execution-time; stopping server"); + std::process::exit(1); + } + }); + handler(); + let _ = done_tx.send(()); + let _ = watchdog.join(); +} + +#[cfg(test)] +mod tests { + use super::*; + + /// No-op handler used by the shutdown-before-accept event-loop fixture. + extern "C" fn noop_handler() {} + + /// Verifies a pre-set portable shutdown flag makes the listener event loop + /// return without accepting a request or leaving a serving thread behind. + #[test] + fn preset_shutdown_stops_event_loop_cleanly() { + static STOP: AtomicBool = AtomicBool::new(true); + serve( + "127.0.0.1:0", + noop_handler, + WorkerConfig { + max_body: 1024, + max_requests: 0, + access_log: false, + max_exec_secs: 0, + gzip: false, + }, + Some(&STOP), + ); } - SERVED.fetch_add(1, Ordering::Relaxed); - request_state::take_body() } diff --git a/docs/README.md b/docs/README.md index f747e6713c..08dc12563f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -69,7 +69,7 @@ Compiler-specific extensions that go beyond standard PHP. These features have no - [FFI & Extern](beyond-php/extern.md) — calling C libraries, extern functions/globals/classes, callbacks - [Conditional Compilation](beyond-php/ifdef.md) — ifdef blocks, compile-time feature flags, CLI flags - [Shared Libraries (cdylib)](beyond-php/cdylib.md) — --emit cdylib, #[Export] C-ABI functions, dlopen lifecycle -- [Web Server (--web)](beyond-php/web.md) — compile a PHP file into a standalone prefork HTTP server binary +- [Web Server (--web)](beyond-php/web.md) — compile a PHP file into a standalone HTTP server binary (Unix prefork or Windows event loop) - [zval Bridge](beyond-php/zval-bridge.md) — zval_pack/unpack/type/free convert elephc values to/from PHP zval structs ## Compiler Internals diff --git a/docs/beyond-php/web.md b/docs/beyond-php/web.md index 166686bd33..92b6f7429d 100644 --- a/docs/beyond-php/web.md +++ b/docs/beyond-php/web.md @@ -1,12 +1,13 @@ --- title: "Web Server (--web)" -description: "Compile a PHP program into a standalone prefork HTTP server binary with --web." +description: "Compile a PHP program into a standalone HTTP server binary with --web." sidebar: order: 7 --- `--web` is an elephc compiler extension: it compiles a standard PHP file into a -standalone prefork HTTP server binary instead of a plain CLI executable. The PHP +standalone HTTP server binary instead of a plain CLI executable. Unix targets use +a prefork supervisor; Windows uses a single-process event loop. The PHP source you compile is standard PHP — the same file would also run under the PHP interpreter or php-fpm — but the compile-and-serve mechanism is specific to elephc. @@ -32,9 +33,10 @@ The produced binary accepts these arguments at runtime: | Argument | Required | Default | Description | |---|---|---|---| | `--listen host:port` | Yes | — | Address and port to bind. Missing `--listen` prints an error to stderr and exits non-zero. | -| `--workers N` | No | CPU count | Number of worker processes to prefork. Minimum 1. | +| `--workers N` | No | CPU count | Number of worker processes to prefork on Unix. Windows always uses one event-loop worker and ignores values other than 1. | | `--max-body-size N` | No | `8388608` (8 MiB) | Max request body in bytes; `0` means unlimited. A request whose body exceeds the cap gets `413 Payload Too Large` and the PHP handler never runs. | -| `--max-requests N` | No | `0` (never) | Recycle each worker after serving N requests (the master respawns it), bounding memory growth in long-running servers. | +| `--max-requests N` | No | `0` (never) | Recycle each Unix worker after N requests; stop the Windows process cleanly after N requests so a service manager can restart it. | +| `--max-execution-time N` | No | `0` (unlimited) | Stop a handler that runs longer than N seconds. Unix recycles that worker; Windows terminates the server process for external restart. | | `--access-log` | No | off | Log one line per request to stderr (` " " `). | | `--help`, `--version` | No | — | Print usage / version and exit 0. | @@ -161,28 +163,32 @@ request to the next. ## Concurrency model -The server uses a prefork model with `SO_REUSEPORT`: the master process forks N -worker processes before any request arrives, and the kernel load-balances -connections across workers. +On Unix, the server uses a prefork model with `SO_REUSEPORT`: the master process +forks N worker processes before any request arrives, and the kernel load-balances +connections across workers. On Windows, one process owns one current-thread +event loop. It multiplexes connection I/O while PHP handlers remain serialized +on that thread; `--workers` does not create extra Windows processes. -Each worker is a separate process with its own copy of the runtime. Within a -single worker, requests are served **one at a time** — the PHP body runs to -completion before the next request is accepted. Parallelism equals the worker -count; a slow request occupies exactly one worker for its duration. +Each Unix worker is a separate process with its own copy of the runtime. On both +models, PHP request bodies run **one at a time per worker/process**. Unix handler +parallelism equals the worker count; Windows currently has one PHP execution +thread, although idle connection I/O is still multiplexed. ## Robustness -- **Graceful shutdown** — the master shuts down cleanly on `SIGINT` (Ctrl-C) and - `SIGTERM`: it forwards termination to the workers, reaps them, and exits `0`. An - in-flight request may be dropped when shutdown arrives. -- **Worker respawn** — a worker that dies unexpectedly (a crash, or PHP calling - `exit()`) is replaced so the pool stays at `--workers` N. +- **Graceful shutdown** — Unix handles `SIGINT`/`SIGTERM`, forwards termination + to workers, reaps them, and exits `0`; Windows handles Ctrl-C and stops its + event loop. An in-flight request may be dropped when shutdown arrives. +- **Worker respawn (Unix)** — a worker that dies unexpectedly (a crash, or PHP + calling `exit()`) is replaced so the pool stays at `--workers` N. Windows relies + on an external service manager after a process-fatal handler failure/timeout. - **Request body cap** — see `--max-body-size`; oversized bodies are rejected with `413` before the handler runs. - **Slow-connection bound** — HTTP/1.1 keep-alive is enabled, but a connection that does not send the next request's headers within 30 s is closed. Because a worker serves one connection at a time, a kept-alive connection holds a worker until it - closes or times out — size `--workers` accordingly. + closes or times out. Size `--workers` accordingly on Unix; Windows multiplexes + idle connection I/O but still serializes active PHP handlers. ## Sessions @@ -237,10 +243,10 @@ available. The following are not yet available: - **`$argc` / `$argv` not populated** — the binary's own argv is consumed by the server and is not forwarded to the script body (PHP-FPM does not set them either). - **No intra-worker concurrency** — `handler()` runs synchronously, so one slow - request occupies its worker until it completes (idle keep-alive connections no - longer block the accept loop, but an in-flight handler does). Use `--workers`. -- **In-flight requests may drop on shutdown** — `SIGINT`/`SIGTERM` terminate - workers promptly; there is no graceful connection drain yet. + request occupies its worker/process until it completes. Use `--workers` on + Unix; Windows currently has no multi-process worker pool. +- **In-flight requests may drop on shutdown** — Unix signals and Windows Ctrl-C + stop promptly; there is no graceful connection drain yet. - **No response streaming** — the whole body is buffered before it is sent. - **`--listen` is TCP only** — Unix-domain-socket listening is not yet supported. - **Not supported in this release:** static file serving, in-process diff --git a/docs/compiling/cli-reference.md b/docs/compiling/cli-reference.md index 64eb91b6d9..e035f76bdb 100644 --- a/docs/compiling/cli-reference.md +++ b/docs/compiling/cli-reference.md @@ -32,7 +32,7 @@ binary is written next to it, named after the source without its extension. | `--source-map` | — | off | Emit a `.map` JSON sidecar next to the assembly ([schema](source-maps.md)). | | `--debug-info` | — | off | Embed DWARF `.file`/`.loc` line directives in the assembly for lldb/gdb/profilers. | | `--php-version VERSION` | `8.2`, `8.3`, `8.4`, `8.5` | `8.5` | Select the maintained PHP compatibility profile for version-dependent behavior. Sessions use it for PHP 8.4 deprecations/validation and PHP 8.5 CHIPS/option semantics. | -| `--web` | — | off | Compile a prefork HTTP server binary instead of a CLI executable. See [Web Server](../beyond-php/web.md). | +| `--web` | — | off | Compile a standalone HTTP server binary instead of a CLI executable. Unix uses prefork workers; Windows uses one event-loop worker. See [Web Server](../beyond-php/web.md). | `--emit-ir`, `--emit-asm`, and `--check` are mutually exclusive. `--web` cannot be combined with `--check`, `--emit cdylib`, `--emit-asm`, or `--emit-ir`. See @@ -46,10 +46,10 @@ runtime arguments (not elephc compiler flags): | Argument | Required | Default | Description | |---|---|---|---| | `--listen host:port` | Yes | — | Address and port to bind. Missing `--listen` prints an error to stderr and exits non-zero. | -| `--workers N` | No | CPU count | Number of prefork worker processes. Minimum 1. | +| `--workers N` | No | CPU count | Number of prefork worker processes on Unix. Windows always uses one event-loop worker and ignores values other than 1. | | `--max-body-size N` | No | `8388608` (8 MiB) | Max request body in bytes (`0` = unlimited); oversized bodies get `413`. | -| `--max-requests N` | No | `0` (never) | Recycle each worker after N requests (bounds memory growth). | -| `--max-execution-time N` | No | `0` (no limit) | Kill and respawn a worker whose request handler runs longer than N seconds. | +| `--max-requests N` | No | `0` (never) | Recycle each Unix worker after N requests; stop the Windows server cleanly after N requests. | +| `--max-execution-time N` | No | `0` (no limit) | Kill and respawn a timed-out Unix worker; terminate the Windows server for external restart. | | `--gzip` | No | off | Compress responses when the client sends `Accept-Encoding: gzip`. | | `--access-log` | No | off | Log one line per request to stderr. | | `--help` (`-h`), `--version` (`-V`) | No | — | Print usage / version and exit. | @@ -61,8 +61,8 @@ elephc --web app.php ``` The served program also receives `$_COOKIE`, `$_REQUEST`, and `$_ENV`, and can -emit cookies with `setcookie()`. The server shuts down cleanly on -`SIGINT`/`SIGTERM` and respawns workers that die. +emit cookies with `setcookie()`. Unix shuts down on `SIGINT`/`SIGTERM` and +respawns failed workers; Windows handles Ctrl-C in its single process. The served program receives the HTTP request through the standard superglobals `$_SERVER`, `$_GET`, `$_POST`, and `php://input`, and controls the response @@ -73,7 +73,7 @@ status and headers with `http_response_code()` and `header()`. See | Flag | Values | Default | Description | |---|---|---|---| -| `--target TARGET` / `--target=TARGET` | `macos-aarch64`, `linux-aarch64`, `linux-x86_64` (plus alias spellings; recognized future targets produce an unsupported-backend diagnostic) | host platform | Select the compilation target. | +| `--target TARGET` / `--target=TARGET` | `macos-aarch64`, `linux-aarch64`, `linux-x86_64`, or experimental `windows-x86_64` (plus alias spellings; recognized `macos-x86_64` remains unsupported) | host platform | Select the compilation target. Windows uses a MinGW ABI/sysroot; GNU tools are the default and an LLVM/LLD path is opt-in. | See [Targets and cross-compilation](targets.md) for the full list of accepted spellings. @@ -81,7 +81,6 @@ spellings. ## Optimization and code generation | Flag | Values | Default | Env override | Description | -|---|---|---|---|---| | `--ir-opt=on\|off` | `on`, `off` | `on` | `ELEPHC_IR_OPT` | Toggle the EIR optimization passes: identity folding, peepholes, constant folding, common-subexpression elimination, loop-invariant code motion, dead-instruction elimination, dead-store elimination, branch simplification, and the cross-function small-function inliner — run to a module-level fixed point. | | `--no-ir-opt` | — | — | `ELEPHC_IR_OPT=off` | Shorthand for `--ir-opt=off`. | | `--regalloc=linear\|stack` | `linear`, `stack` | `linear` | `ELEPHC_REGALLOC` | Register allocator: linear-scan, or stack-only fallback. | @@ -171,3 +170,13 @@ changing every invocation: | `ELEPHC_IR_OPT` | `on`, `off` | `--ir-opt=` | | `ELEPHC_REGALLOC` | `linear`, `stack` | `--regalloc=` | | `ELEPHC_NULL_REPR` | `tagged`, `sentinel` | `--null-repr=` | + +Windows cross-compilation also accepts toolchain-only environment variables: + +| Variable | Purpose | +|---|---| +| `ELEPHC_WINDOWS_TOOLCHAIN` | `gnu` (default) or `llvm`. | +| `ELEPHC_WINDOWS_SYSROOT` | MinGW sysroot used by LLVM mode for CRT objects and import libraries. | +| `ELEPHC_WINDOWS_GCC` | Override the GNU MinGW driver used directly and for sysroot discovery. | +| `ELEPHC_WINDOWS_CLANG` | Override the Clang executable used in LLVM mode. | +| `ELEPHC_WINDOWS_LLD` | Override the linker name/path passed through Clang's `-fuse-ld`. | diff --git a/docs/compiling/targets.md b/docs/compiling/targets.md index 08cf19f1d2..d40f959dc4 100644 --- a/docs/compiling/targets.md +++ b/docs/compiling/targets.md @@ -40,11 +40,34 @@ scripts written for other toolchains keep working: | `linux-aarch64` | `linux-arm64`, `aarch64-unknown-linux-gnu` | | `linux-x86_64` | `x86_64-unknown-linux-gnu` | -The parser also recognizes `macos-x86_64` / `x86_64-apple-darwin` and -`windows-x86_64` / `x86_64-pc-windows-msvc` / -`x86_64-pc-windows-gnu`. These spellings are groundwork for future backends, -not supported targets: compilation stops with an explicit unsupported-backend -diagnostic. +The parser also recognizes `macos-x86_64` / `x86_64-apple-darwin`, but that +backend is not supported and compilation stops with an explicit diagnostic. + +`windows-x86_64`, `x86_64-pc-windows-msvc`, and +`x86_64-pc-windows-gnu` currently select the experimental Windows x86-64 +backend. Despite accepting the MSVC spelling for CLI compatibility, the backend +currently emits and links with the GNU/MinGW ABI; do not use that spelling as an +MSVC ABI guarantee. See the parity gate and current limitations below. + +The GNU MinGW assembler and GCC driver remain the default Windows toolchain. An +opt-in LLVM path uses Clang's integrated assembler and LLD while retaining that +same GNU Windows ABI: + +```bash +ELEPHC_WINDOWS_TOOLCHAIN=llvm elephc --target windows-x86_64 hello.php +``` + +LLVM mode needs a MinGW sysroot for CRT objects and import libraries. elephc +normally obtains it from `x86_64-w64-mingw32-gcc -print-sysroot`; set +`ELEPHC_WINDOWS_SYSROOT` when using a standalone gnullvm layout. The executable +names can be overridden with `ELEPHC_WINDOWS_CLANG`, `ELEPHC_WINDOWS_LLD`, and +`ELEPHC_WINDOWS_GCC`. `ELEPHC_WINDOWS_LLD` is passed to Clang's `-fuse-ld` +option, so it may name `lld`, `ld.lld`, or an absolute `rust-lld` path. + +This toolchain selection does not claim Control Flow Guard support. elephc keeps +the PE `GUARD_CF` characteristic clear until indirect-call instrumentation, a +complete GFIDS table, and a valid load-configuration directory are emitted and +verified together. ## Cross-compilation notes @@ -57,3 +80,66 @@ 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 gate + +`windows-x86_64` is an experimental target, not yet a first-class supported +target. CI nevertheless treats its codegen suite as a strict native gate: every +runnable `ci`-profile codegen fixture is compiled and executed directly on +Windows Server 2025. + +### How the gate works + +The `windows-codegen-native` job splits the runnable inventory into 8 +deterministic hash partitions, each running two nextest workers. Each shard runs +through `cargo nextest` with no failure baseline: one failing fixture makes that +shard fail. There is no allow-list or known-failures mechanism in this gate. + +Every shard uploads its JUnit report, and shard 1 also uploads the complete +runnable inventory. The aggregating `windows-codegen-gate` job then verifies +that: + +1. all 8 reports are present; +2. every runnable fixture appears exactly once across those reports; +3. no reported test failed; and +4. all native shard jobs succeeded. + +This prevents both test failures and missing or truncated partitions from being +accepted as a green Windows result. The aggregate gate is required by the +top-level `test` job. + +## Windows bridge verification + +CI builds and tests the image, PDO, Phar, crypto, timezone, TLS, and web bridges +on a native Windows host using Rust's MSVC host toolchain. It also builds their +GNU/COFF static archives for `x86_64-pc-windows-gnu` and verifies the exported C +ABI against every `#[no_mangle] pub extern "C"` entry point declared in the +bridge sources. + +The native job also runs the complete dedicated `windows_pe` integration module +and the bridge unit tests. TLS attachment uses full-width Winsock `SOCKET` +values and duplicates the live socket before rustls adopts it; the native bridge +tests verify that ownership boundary. Magician is built with the native MinGW +PCRE2 sysroot and exercised by the eval fixtures in the strict codegen shards. +The web bridge's PE tests cover its exported C ABI, successful and bad-request +HTTP responses, and clean `--max-requests` shutdown. + +These checks are necessary but are not by themselves a promotion signal. +Windows stays experimental until the first complete strict native run is green +and the target-policy review confirms that no Windows-only reduced semantics +remain. + +### Promotion checklist + +Promotion is a deliberate documentation and CI change after native validation, +not an automatic consequence of one green shard run. It requires all of the +following from the same revision: + +1. all 8 strict native shards pass and the aggregate coverage check accounts + for every runnable `ci`-profile codegen fixture exactly once; +2. the native Windows PE, bridge, eval, and web suites pass; +3. bridge export verification and GNU plus LLVM/LLD toolchain probes pass; and +4. the target-policy review confirms no Windows-only reduced semantics remain. + +Until then, keep Windows labeled experimental in the target table, README, CLI +reference, architecture guide, developer policy, changelog, and CI job comments. diff --git a/docs/internals/architecture.md b/docs/internals/architecture.md index e7d1b520c5..68b0de01e1 100644 --- a/docs/internals/architecture.md +++ b/docs/internals/architecture.md @@ -170,7 +170,20 @@ The compiler now distinguishes the operating-system side of a target from the in - `Arch` describes the instruction set and calling convention such as `AArch64` vs `X86_64`. - `Target` combines both and is threaded from the CLI into codegen and the test harness. -AArch64 remains the most established and best-documented backend (macOS and Linux), and the explicit `Target` model now also covers Linux `x86_64` with its own ABI/runtime slices. The `Target` split lets each ISA live alongside the others without reintroducing the old assumption that `Linux` automatically means ARM64. +AArch64 remains the most established and best-documented backend (macOS and +Linux), and the explicit `Target` model also covers Linux `x86_64` plus the +experimental Windows `x86_64` PE backend. Generated Windows functions and +C-facing calls follow Microsoft x64 register, shadow-space, and non-volatile +register rules. Handwritten x86_64 runtime helpers retain an internal +SysV-shaped convention where documented, with Win32 shims and centralized +native/published-bridge call adapters owning every ABI transition. The `Target` +split keeps ISA, operating-system, object-format, and calling-convention choices +explicit instead of treating `Linux` or `x86_64` as a proxy for all four. + +`--web` follows the same target split: Unix binaries use a prefork supervisor, +while Windows PE binaries use one current-thread event loop and one serialized +PHP execution thread. Both link the same `elephc-web` C ABI and request/response +state surface. ## Module map @@ -204,6 +217,7 @@ src/ ├── 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/ Active EIR to target assembly backend ├── codegen_support/ Shared ABI, runtime, platform, metadata, and callable support +├── windows_toolchain.rs MinGW versus LLVM/LLD discovery, sysroot probing, and target command selection ├── runtime_cache.rs Cached shared runtime object preparation ├── source_map.rs Assembly comment markers → JSON sidecar map ├── debug_info.rs DWARF debug-info injection for `--debug-info` (lldb/gdb source mapping) @@ -328,12 +342,13 @@ src/ │ │ ├── registers.rs Register names per architecture │ │ ├── symbols.rs Symbol / literal address loading │ │ ├── tests.rs ABI unit-test module root -│ │ ├── tests/ ABI unit tests (`basics.rs`, `arguments.rs`, `symbols.rs`, `linux_x86_64.rs`) +│ │ ├── tests/ ABI unit tests (`basics.rs`, `arguments.rs`, `callbacks.rs`, `symbols.rs`, `linux_x86_64.rs`) │ │ └── values.rs Push/pop/load/store by PhpType -│ ├── platform/ Target selection and Linux transforms +│ ├── platform/ Target selection plus Linux and Windows assembly transforms │ │ ├── mod.rs Platform module root, re-exports Platform / Arch / Target │ │ ├── target.rs Platform / Arch / Target definitions and derived codegen properties │ │ ├── linux_transform.rs Linux post-emit transforms, syscall mapping, C-symbol remapping +│ │ ├── windows_transform.rs PE/COFF syscall, import, and conservative SEH-unwind transformation │ │ └── toolchain.rs Assembler / linker invocation │ ├── cdylib.rs C-ABI export trampolines + lifecycle symbols for --emit cdylib │ ├── visibility.rs ELF .hidden directives for internal globals in cdylib emission @@ -348,19 +363,20 @@ src/ │ ├── eval_bridge.rs C-ABI value, callable, class, and runtime hooks used by Magician │ ├── eval_scope.rs Core materialized-scope helpers usable without the interpreter │ ├── emitters.rs `emit_runtime()` orchestration — emits every runtime category in a fixed order -│ ├── strings/ itoa, concat, resource display, ftoa, sprintf, md5, sha1, str_persist, ... (72 files) -│ ├── arrays/ heap_alloc, heap_free, array_free_deep, array_grow, hash_grow, hash_*, mixed boxing/freeing, mixed instanceof, sort, usort, refcount, gc/decref dispatch, ... (148 files) +│ ├── strings/ itoa, concat, shell escaping, resource display, ftoa, sprintf, md5, sha1, str_persist, ... (74 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, ... (149 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, ... (117 files) +│ ├── io/ fopen, fgets, fread, stat, streams, sockets, process descriptors/status, filters, scandir, ... (123 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) +│ ├── exceptions/ cleanup_frames, dynamic/static throws, matches, throw_current, rethrow_current, class_implements helpers (8 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_*, ... (43 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 fiber-backed coroutine __rt_gen_* helpers (3 files) +│ ├── win32/ PE imports plus filesystem, network, encoding, error, time, compression, PCRE, and C-symbol shims (12 files) │ └── zval/ Zval bridge packing, unpacking, type, and lifetime helpers (11 files) │ │ diff --git a/docs/internals/builtins/_internal/__elephc_gmmktime_raw.md b/docs/internals/builtins/_internal/__elephc_gmmktime_raw.md index 7cce3b9416..69ea6afe46 100644 --- a/docs/internals/builtins/_internal/__elephc_gmmktime_raw.md +++ b/docs/internals/builtins/_internal/__elephc_gmmktime_raw.md @@ -2,7 +2,7 @@ title: "__elephc_gmmktime_raw() — internals" description: "Compiler internals for __elephc_gmmktime_raw(): lowering path, type checks, and runtime helpers." sidebar: - order: 458 + order: 465 --- ## `__elephc_gmmktime_raw()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/__elephc_gmmktime_raw.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/__elephc_gmmktime_raw.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_mktime_raw.md b/docs/internals/builtins/_internal/__elephc_mktime_raw.md index 53e3d0c87f..61c1eda74d 100644 --- a/docs/internals/builtins/_internal/__elephc_mktime_raw.md +++ b/docs/internals/builtins/_internal/__elephc_mktime_raw.md @@ -2,7 +2,7 @@ title: "__elephc_mktime_raw() — internals" description: "Compiler internals for __elephc_mktime_raw(): lowering path, type checks, and runtime helpers." sidebar: - order: 459 + order: 466 --- ## `__elephc_mktime_raw()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/__elephc_mktime_raw.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/__elephc_mktime_raw.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_bzip2_archive.md b/docs/internals/builtins/_internal/__elephc_phar_bzip2_archive.md index c14a4fc006..3f6a8088db 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_bzip2_archive.md +++ b/docs/internals/builtins/_internal/__elephc_phar_bzip2_archive.md @@ -2,7 +2,7 @@ title: "__elephc_phar_bzip2_archive() — internals" description: "Compiler internals for __elephc_phar_bzip2_archive(): lowering path, type checks, and runtime helpers." sidebar: - order: 460 + order: 467 --- ## `__elephc_phar_bzip2_archive()` — internals @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_decompress_archive.md b/docs/internals/builtins/_internal/__elephc_phar_decompress_archive.md index 18f1e94aee..83a4bded8c 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_decompress_archive.md +++ b/docs/internals/builtins/_internal/__elephc_phar_decompress_archive.md @@ -2,7 +2,7 @@ title: "__elephc_phar_decompress_archive() — internals" description: "Compiler internals for __elephc_phar_decompress_archive(): lowering path, type checks, and runtime helpers." sidebar: - order: 461 + order: 468 --- ## `__elephc_phar_decompress_archive()` — internals @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_get_file_metadata.md b/docs/internals/builtins/_internal/__elephc_phar_get_file_metadata.md index ba141c8272..b39a97a65d 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_get_file_metadata.md +++ b/docs/internals/builtins/_internal/__elephc_phar_get_file_metadata.md @@ -2,7 +2,7 @@ title: "__elephc_phar_get_file_metadata() — internals" description: "Compiler internals for __elephc_phar_get_file_metadata(): lowering path, type checks, and runtime helpers." sidebar: - order: 462 + order: 469 --- ## `__elephc_phar_get_file_metadata()` — internals @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_get_metadata.md b/docs/internals/builtins/_internal/__elephc_phar_get_metadata.md index 24462565ff..e225157363 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_get_metadata.md +++ b/docs/internals/builtins/_internal/__elephc_phar_get_metadata.md @@ -2,7 +2,7 @@ title: "__elephc_phar_get_metadata() — internals" description: "Compiler internals for __elephc_phar_get_metadata(): lowering path, type checks, and runtime helpers." sidebar: - order: 463 + order: 470 --- ## `__elephc_phar_get_metadata()` — internals @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_get_signature_hash.md b/docs/internals/builtins/_internal/__elephc_phar_get_signature_hash.md index 0a0310a5df..30a25146be 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_get_signature_hash.md +++ b/docs/internals/builtins/_internal/__elephc_phar_get_signature_hash.md @@ -2,7 +2,7 @@ title: "__elephc_phar_get_signature_hash() — internals" description: "Compiler internals for __elephc_phar_get_signature_hash(): lowering path, type checks, and runtime helpers." sidebar: - order: 464 + order: 471 --- ## `__elephc_phar_get_signature_hash()` — internals @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_get_signature_type.md b/docs/internals/builtins/_internal/__elephc_phar_get_signature_type.md index 7a4001d358..76a5cfcb8a 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_get_signature_type.md +++ b/docs/internals/builtins/_internal/__elephc_phar_get_signature_type.md @@ -2,7 +2,7 @@ title: "__elephc_phar_get_signature_type() — internals" description: "Compiler internals for __elephc_phar_get_signature_type(): lowering path, type checks, and runtime helpers." sidebar: - order: 465 + order: 472 --- ## `__elephc_phar_get_signature_type()` — internals @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_get_stub.md b/docs/internals/builtins/_internal/__elephc_phar_get_stub.md index f362133f4b..6069b8a849 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_get_stub.md +++ b/docs/internals/builtins/_internal/__elephc_phar_get_stub.md @@ -2,7 +2,7 @@ title: "__elephc_phar_get_stub() — internals" description: "Compiler internals for __elephc_phar_get_stub(): lowering path, type checks, and runtime helpers." sidebar: - order: 466 + order: 473 --- ## `__elephc_phar_get_stub()` — internals @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_gzip_archive.md b/docs/internals/builtins/_internal/__elephc_phar_gzip_archive.md index 1876cc845e..8ff71d18e0 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_gzip_archive.md +++ b/docs/internals/builtins/_internal/__elephc_phar_gzip_archive.md @@ -2,7 +2,7 @@ title: "__elephc_phar_gzip_archive() — internals" description: "Compiler internals for __elephc_phar_gzip_archive(): lowering path, type checks, and runtime helpers." sidebar: - order: 467 + order: 474 --- ## `__elephc_phar_gzip_archive()` — internals @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_list_entries.md b/docs/internals/builtins/_internal/__elephc_phar_list_entries.md index b1897c9fcd..ba3793ce7d 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_list_entries.md +++ b/docs/internals/builtins/_internal/__elephc_phar_list_entries.md @@ -2,7 +2,7 @@ title: "__elephc_phar_list_entries() — internals" description: "Compiler internals for __elephc_phar_list_entries(): lowering path, type checks, and runtime helpers." sidebar: - order: 468 + order: 475 --- ## `__elephc_phar_list_entries()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_set_compression.md b/docs/internals/builtins/_internal/__elephc_phar_set_compression.md index 1ccc1112ea..35afde5f22 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_set_compression.md +++ b/docs/internals/builtins/_internal/__elephc_phar_set_compression.md @@ -2,7 +2,7 @@ title: "__elephc_phar_set_compression() — internals" description: "Compiler internals for __elephc_phar_set_compression(): lowering path, type checks, and runtime helpers." sidebar: - order: 469 + order: 476 --- ## `__elephc_phar_set_compression()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_set_file_metadata.md b/docs/internals/builtins/_internal/__elephc_phar_set_file_metadata.md index 5d3a2858b9..d56b152758 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_set_file_metadata.md +++ b/docs/internals/builtins/_internal/__elephc_phar_set_file_metadata.md @@ -2,7 +2,7 @@ title: "__elephc_phar_set_file_metadata() — internals" description: "Compiler internals for __elephc_phar_set_file_metadata(): lowering path, type checks, and runtime helpers." sidebar: - order: 470 + order: 477 --- ## `__elephc_phar_set_file_metadata()` — internals @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_set_metadata.md b/docs/internals/builtins/_internal/__elephc_phar_set_metadata.md index 7f77975181..5ef5f62090 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_set_metadata.md +++ b/docs/internals/builtins/_internal/__elephc_phar_set_metadata.md @@ -2,7 +2,7 @@ title: "__elephc_phar_set_metadata() — internals" description: "Compiler internals for __elephc_phar_set_metadata(): lowering path, type checks, and runtime helpers." sidebar: - order: 471 + order: 478 --- ## `__elephc_phar_set_metadata()` — internals @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_set_stub.md b/docs/internals/builtins/_internal/__elephc_phar_set_stub.md index cd59ad271a..7bb499feac 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_set_stub.md +++ b/docs/internals/builtins/_internal/__elephc_phar_set_stub.md @@ -2,7 +2,7 @@ title: "__elephc_phar_set_stub() — internals" description: "Compiler internals for __elephc_phar_set_stub(): lowering path, type checks, and runtime helpers." sidebar: - order: 472 + order: 479 --- ## `__elephc_phar_set_stub()` — internals @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_set_zip_password.md b/docs/internals/builtins/_internal/__elephc_phar_set_zip_password.md index 4e87995135..84c6fd919a 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_set_zip_password.md +++ b/docs/internals/builtins/_internal/__elephc_phar_set_zip_password.md @@ -2,7 +2,7 @@ title: "__elephc_phar_set_zip_password() — internals" description: "Compiler internals for __elephc_phar_set_zip_password(): lowering path, type checks, and runtime helpers." sidebar: - order: 473 + order: 480 --- ## `__elephc_phar_set_zip_password()` — internals @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_sign_hash.md b/docs/internals/builtins/_internal/__elephc_phar_sign_hash.md index cb623d645e..96a4079680 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_sign_hash.md +++ b/docs/internals/builtins/_internal/__elephc_phar_sign_hash.md @@ -2,7 +2,7 @@ title: "__elephc_phar_sign_hash() — internals" description: "Compiler internals for __elephc_phar_sign_hash(): lowering path, type checks, and runtime helpers." sidebar: - order: 474 + order: 481 --- ## `__elephc_phar_sign_hash()` — internals @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function __elephc_phar_sign_hash(string $path, string $algo): bool +function __elephc_phar_sign_hash(string $path, int $algo): bool ``` ## What the type checker enforces diff --git a/docs/internals/builtins/_internal/__elephc_phar_sign_openssl.md b/docs/internals/builtins/_internal/__elephc_phar_sign_openssl.md index 9e2de42789..3a236471e9 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_sign_openssl.md +++ b/docs/internals/builtins/_internal/__elephc_phar_sign_openssl.md @@ -2,7 +2,7 @@ title: "__elephc_phar_sign_openssl() — internals" description: "Compiler internals for __elephc_phar_sign_openssl(): lowering path, type checks, and runtime helpers." sidebar: - order: 475 + order: 482 --- ## `__elephc_phar_sign_openssl()` — internals @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_ptr_is_null.md b/docs/internals/builtins/_internal/__elephc_ptr_is_null.md index 50ff2ae79a..ecbbb7bb9d 100644 --- a/docs/internals/builtins/_internal/__elephc_ptr_is_null.md +++ b/docs/internals/builtins/_internal/__elephc_ptr_is_null.md @@ -2,7 +2,7 @@ title: "__elephc_ptr_is_null() — internals" description: "Compiler internals for __elephc_ptr_is_null(): lowering path, type checks, and runtime helpers." sidebar: - order: 476 + order: 483 --- ## `__elephc_ptr_is_null()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/__elephc_ptr_is_null.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/__elephc_ptr_is_null.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_ptr_read_string.md b/docs/internals/builtins/_internal/__elephc_ptr_read_string.md index 454d14d347..94952431c9 100644 --- a/docs/internals/builtins/_internal/__elephc_ptr_read_string.md +++ b/docs/internals/builtins/_internal/__elephc_ptr_read_string.md @@ -2,7 +2,7 @@ title: "__elephc_ptr_read_string() — internals" description: "Compiler internals for __elephc_ptr_read_string(): lowering path, type checks, and runtime helpers." sidebar: - order: 477 + order: 484 --- ## `__elephc_ptr_read_string()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/__elephc_ptr_read_string.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/__elephc_ptr_read_string.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_ptr_write_string.md b/docs/internals/builtins/_internal/__elephc_ptr_write_string.md index 9696a92a45..63998595c3 100644 --- a/docs/internals/builtins/_internal/__elephc_ptr_write_string.md +++ b/docs/internals/builtins/_internal/__elephc_ptr_write_string.md @@ -2,7 +2,7 @@ title: "__elephc_ptr_write_string() — internals" description: "Compiler internals for __elephc_ptr_write_string(): lowering path, type checks, and runtime helpers." sidebar: - order: 478 + order: 485 --- ## `__elephc_ptr_write_string()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/__elephc_ptr_write_string.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/__elephc_ptr_write_string.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_strtotime_raw.md b/docs/internals/builtins/_internal/__elephc_strtotime_raw.md index 6e0cce06d5..00852b6cc1 100644 --- a/docs/internals/builtins/_internal/__elephc_strtotime_raw.md +++ b/docs/internals/builtins/_internal/__elephc_strtotime_raw.md @@ -2,7 +2,7 @@ title: "__elephc_strtotime_raw() — internals" description: "Compiler internals for __elephc_strtotime_raw(): lowering path, type checks, and runtime helpers." sidebar: - order: 479 + order: 486 --- ## `__elephc_strtotime_raw()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/__elephc_strtotime_raw.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/__elephc_strtotime_raw.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -29,7 +29,7 @@ sidebar: - **Result type source**: `declared` - **Result ownership**: `may_alias_arguments` - **Effects**: `static (16 declared effects)` -- **Requirements**: `static (0 requirements)` +- **Requirements**: `shared` - **Callable policy**: `static_only` - **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` diff --git a/docs/internals/builtins/array/array_all.md b/docs/internals/builtins/array/array_all.md index a37900918f..c268edb182 100644 --- a/docs/internals/builtins/array/array_all.md +++ b/docs/internals/builtins/array/array_all.md @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_any.md b/docs/internals/builtins/array/array_any.md index fe2e3b0fca..ae5ebae8cc 100644 --- a/docs/internals/builtins/array/array_any.md +++ b/docs/internals/builtins/array/array_any.md @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_chunk.md b/docs/internals/builtins/array/array_chunk.md index dc5277591e..c10fcba07f 100644 --- a/docs/internals/builtins/array/array_chunk.md +++ b/docs/internals/builtins/array/array_chunk.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_chunk.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_chunk.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_column.md b/docs/internals/builtins/array/array_column.md index c3345bd640..d5e0234a14 100644 --- a/docs/internals/builtins/array/array_column.md +++ b/docs/internals/builtins/array/array_column.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_column.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_column.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_combine.md b/docs/internals/builtins/array/array_combine.md index babc5aba2b..61478bea9c 100644 --- a/docs/internals/builtins/array/array_combine.md +++ b/docs/internals/builtins/array/array_combine.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_combine.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_combine.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_diff.md b/docs/internals/builtins/array/array_diff.md index 671f8b2836..fe71452b03 100644 --- a/docs/internals/builtins/array/array_diff.md +++ b/docs/internals/builtins/array/array_diff.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_diff.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_diff.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_diff_assoc.md b/docs/internals/builtins/array/array_diff_assoc.md index 783fbcc4a1..4d78a19faa 100644 --- a/docs/internals/builtins/array/array_diff_assoc.md +++ b/docs/internals/builtins/array/array_diff_assoc.md @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_diff_key.md b/docs/internals/builtins/array/array_diff_key.md index c1d276a820..c2349d818b 100644 --- a/docs/internals/builtins/array/array_diff_key.md +++ b/docs/internals/builtins/array/array_diff_key.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_diff_key.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_diff_key.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_fill.md b/docs/internals/builtins/array/array_fill.md index 37c6bc56a6..e9cdc2aef4 100644 --- a/docs/internals/builtins/array/array_fill.md +++ b/docs/internals/builtins/array/array_fill.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_fill.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_fill.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_fill_keys.md b/docs/internals/builtins/array/array_fill_keys.md index 4d6fb301b2..994e30089d 100644 --- a/docs/internals/builtins/array/array_fill_keys.md +++ b/docs/internals/builtins/array/array_fill_keys.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_fill_keys.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_fill_keys.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_filter.md b/docs/internals/builtins/array/array_filter.md index 0076d873d6..caebbeed53 100644 --- a/docs/internals/builtins/array/array_filter.md +++ b/docs/internals/builtins/array/array_filter.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_filter.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_filter.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_find.md b/docs/internals/builtins/array/array_find.md index 13c0c4ab3e..0b55706254 100644 --- a/docs/internals/builtins/array/array_find.md +++ b/docs/internals/builtins/array/array_find.md @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_flip.md b/docs/internals/builtins/array/array_flip.md index 310d50da01..567f359146 100644 --- a/docs/internals/builtins/array/array_flip.md +++ b/docs/internals/builtins/array/array_flip.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_flip.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_flip.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_intersect.md b/docs/internals/builtins/array/array_intersect.md index 7b6cf988b7..6b7351efea 100644 --- a/docs/internals/builtins/array/array_intersect.md +++ b/docs/internals/builtins/array/array_intersect.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_intersect.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_intersect.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_intersect_assoc.md b/docs/internals/builtins/array/array_intersect_assoc.md index 6212873829..6160ce7792 100644 --- a/docs/internals/builtins/array/array_intersect_assoc.md +++ b/docs/internals/builtins/array/array_intersect_assoc.md @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_intersect_key.md b/docs/internals/builtins/array/array_intersect_key.md index c404e06395..57b854a7cf 100644 --- a/docs/internals/builtins/array/array_intersect_key.md +++ b/docs/internals/builtins/array/array_intersect_key.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_intersect_key.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_intersect_key.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_is_list.md b/docs/internals/builtins/array/array_is_list.md index 4cd7bf9d2c..55a35a93fd 100644 --- a/docs/internals/builtins/array/array_is_list.md +++ b/docs/internals/builtins/array/array_is_list.md @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_key_exists.md b/docs/internals/builtins/array/array_key_exists.md index 279abe835c..b54fd1147a 100644 --- a/docs/internals/builtins/array/array_key_exists.md +++ b/docs/internals/builtins/array/array_key_exists.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_key_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_key_exists.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_key_first.md b/docs/internals/builtins/array/array_key_first.md index b79ebeea38..09ca02b8f1 100644 --- a/docs/internals/builtins/array/array_key_first.md +++ b/docs/internals/builtins/array/array_key_first.md @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_key_last.md b/docs/internals/builtins/array/array_key_last.md index 730431da87..9ed285de86 100644 --- a/docs/internals/builtins/array/array_key_last.md +++ b/docs/internals/builtins/array/array_key_last.md @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_keys.md b/docs/internals/builtins/array/array_keys.md index 25f228afc1..619d2a7720 100644 --- a/docs/internals/builtins/array/array_keys.md +++ b/docs/internals/builtins/array/array_keys.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_keys.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_keys.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_map.md b/docs/internals/builtins/array/array_map.md index 0232a5c6a9..871bc8f6f3 100644 --- a/docs/internals/builtins/array/array_map.md +++ b/docs/internals/builtins/array/array_map.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_map.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_map.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_merge.md b/docs/internals/builtins/array/array_merge.md index 56ad5dad6a..98b9f995d7 100644 --- a/docs/internals/builtins/array/array_merge.md +++ b/docs/internals/builtins/array/array_merge.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_merge.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_merge.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_merge_recursive.md b/docs/internals/builtins/array/array_merge_recursive.md index 4ec6eefd70..becd25e6c5 100644 --- a/docs/internals/builtins/array/array_merge_recursive.md +++ b/docs/internals/builtins/array/array_merge_recursive.md @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_multisort.md b/docs/internals/builtins/array/array_multisort.md index 6c6deba2bf..c09c7c2f6a 100644 --- a/docs/internals/builtins/array/array_multisort.md +++ b/docs/internals/builtins/array/array_multisort.md @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function array_multisort(array $array1, int $array2): bool +function array_multisort(array &$array1, int &$array2): bool ``` ## What the type checker enforces diff --git a/docs/internals/builtins/array/array_pad.md b/docs/internals/builtins/array/array_pad.md index 0a6d98264f..db2b508bc7 100644 --- a/docs/internals/builtins/array/array_pad.md +++ b/docs/internals/builtins/array/array_pad.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_pad.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_pad.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_pop.md b/docs/internals/builtins/array/array_pop.md index f8e956bfbf..908470da4a 100644 --- a/docs/internals/builtins/array/array_pop.md +++ b/docs/internals/builtins/array/array_pop.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_pop.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_pop.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function array_pop(array $array): mixed +function array_pop(array &$array): mixed ``` ## What the type checker enforces diff --git a/docs/internals/builtins/array/array_product.md b/docs/internals/builtins/array/array_product.md index 10f8817db6..1ec53aa9a5 100644 --- a/docs/internals/builtins/array/array_product.md +++ b/docs/internals/builtins/array/array_product.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_product.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_product.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_push.md b/docs/internals/builtins/array/array_push.md index b4669bcc56..91860e3f4f 100644 --- a/docs/internals/builtins/array/array_push.md +++ b/docs/internals/builtins/array/array_push.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_push.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_push.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function array_push(array $array, ...$values): void +function array_push(array &$array, ...$values): void ``` ## What the type checker enforces diff --git a/docs/internals/builtins/array/array_rand.md b/docs/internals/builtins/array/array_rand.md index d2b7868097..0b0fb7072c 100644 --- a/docs/internals/builtins/array/array_rand.md +++ b/docs/internals/builtins/array/array_rand.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_rand.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_rand.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_reduce.md b/docs/internals/builtins/array/array_reduce.md index 4a9440b385..56257b4434 100644 --- a/docs/internals/builtins/array/array_reduce.md +++ b/docs/internals/builtins/array/array_reduce.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_reduce.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_reduce.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_replace.md b/docs/internals/builtins/array/array_replace.md index 464579963e..b4c56e825a 100644 --- a/docs/internals/builtins/array/array_replace.md +++ b/docs/internals/builtins/array/array_replace.md @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_replace_recursive.md b/docs/internals/builtins/array/array_replace_recursive.md index 2ca9b37ac7..6c64ebea30 100644 --- a/docs/internals/builtins/array/array_replace_recursive.md +++ b/docs/internals/builtins/array/array_replace_recursive.md @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_reverse.md b/docs/internals/builtins/array/array_reverse.md index c5ec00f202..40dc79f254 100644 --- a/docs/internals/builtins/array/array_reverse.md +++ b/docs/internals/builtins/array/array_reverse.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_reverse.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_reverse.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_search.md b/docs/internals/builtins/array/array_search.md index 7ceaae13ab..3daffa501e 100644 --- a/docs/internals/builtins/array/array_search.md +++ b/docs/internals/builtins/array/array_search.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_search.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_search.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_shift.md b/docs/internals/builtins/array/array_shift.md index aae3a1b23f..71e94e7126 100644 --- a/docs/internals/builtins/array/array_shift.md +++ b/docs/internals/builtins/array/array_shift.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_shift.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_shift.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function array_shift(array $array): mixed +function array_shift(array &$array): mixed ``` ## What the type checker enforces diff --git a/docs/internals/builtins/array/array_slice.md b/docs/internals/builtins/array/array_slice.md index 615c33994d..8259219e67 100644 --- a/docs/internals/builtins/array/array_slice.md +++ b/docs/internals/builtins/array/array_slice.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_slice.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_slice.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_splice.md b/docs/internals/builtins/array/array_splice.md index 7f91c52248..e83c27a44d 100644 --- a/docs/internals/builtins/array/array_splice.md +++ b/docs/internals/builtins/array/array_splice.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_splice.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_splice.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function array_splice(array $array, int $offset, int $length = null): array +function array_splice(array &$array, int $offset, int $length = null): array ``` ## What the type checker enforces diff --git a/docs/internals/builtins/array/array_sum.md b/docs/internals/builtins/array/array_sum.md index 47e681128c..564bbb9b78 100644 --- a/docs/internals/builtins/array/array_sum.md +++ b/docs/internals/builtins/array/array_sum.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_sum.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_sum.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_udiff.md b/docs/internals/builtins/array/array_udiff.md index 9f3605ea38..e951c61601 100644 --- a/docs/internals/builtins/array/array_udiff.md +++ b/docs/internals/builtins/array/array_udiff.md @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_uintersect.md b/docs/internals/builtins/array/array_uintersect.md index 0bfd9a06a6..3be5b3eb2a 100644 --- a/docs/internals/builtins/array/array_uintersect.md +++ b/docs/internals/builtins/array/array_uintersect.md @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_unique.md b/docs/internals/builtins/array/array_unique.md index baabff417c..408517e50b 100644 --- a/docs/internals/builtins/array/array_unique.md +++ b/docs/internals/builtins/array/array_unique.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_unique.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_unique.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_unshift.md b/docs/internals/builtins/array/array_unshift.md index d888b18ac2..5a5422eae0 100644 --- a/docs/internals/builtins/array/array_unshift.md +++ b/docs/internals/builtins/array/array_unshift.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_unshift.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_unshift.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function array_unshift(array $array, ...$values): int +function array_unshift(array &$array, ...$values): int ``` ## What the type checker enforces diff --git a/docs/internals/builtins/array/array_values.md b/docs/internals/builtins/array/array_values.md index 7e541e5b48..183076fa41 100644 --- a/docs/internals/builtins/array/array_values.md +++ b/docs/internals/builtins/array/array_values.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_values.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_values.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_walk.md b/docs/internals/builtins/array/array_walk.md index 233f713813..97cb04826f 100644 --- a/docs/internals/builtins/array/array_walk.md +++ b/docs/internals/builtins/array/array_walk.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_walk.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_walk.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function array_walk(array $array, callable $callback): void +function array_walk(array &$array, callable $callback): void ``` ## What the type checker enforces diff --git a/docs/internals/builtins/array/array_walk_recursive.md b/docs/internals/builtins/array/array_walk_recursive.md index 10c0e56f5a..8fb044c015 100644 --- a/docs/internals/builtins/array/array_walk_recursive.md +++ b/docs/internals/builtins/array/array_walk_recursive.md @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function array_walk_recursive(array $array, callable $callback): void +function array_walk_recursive(array &$array, callable $callback): void ``` ## What the type checker enforces diff --git a/docs/internals/builtins/array/arsort.md b/docs/internals/builtins/array/arsort.md index 8f5f621809..7b6c57d5f4 100644 --- a/docs/internals/builtins/array/arsort.md +++ b/docs/internals/builtins/array/arsort.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/arsort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/arsort.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function arsort(array $array): bool +function arsort(array &$array): bool ``` ## What the type checker enforces diff --git a/docs/internals/builtins/array/asort.md b/docs/internals/builtins/array/asort.md index f507229414..5cea1c4b41 100644 --- a/docs/internals/builtins/array/asort.md +++ b/docs/internals/builtins/array/asort.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/asort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/asort.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function asort(array $array): bool +function asort(array &$array): bool ``` ## What the type checker enforces diff --git a/docs/internals/builtins/array/call_user_func.md b/docs/internals/builtins/array/call_user_func.md index 062f224927..2fc251c3db 100644 --- a/docs/internals/builtins/array/call_user_func.md +++ b/docs/internals/builtins/array/call_user_func.md @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/call_user_func_array.md b/docs/internals/builtins/array/call_user_func_array.md index e03fb70228..60e63a17e3 100644 --- a/docs/internals/builtins/array/call_user_func_array.md +++ b/docs/internals/builtins/array/call_user_func_array.md @@ -10,7 +10,7 @@ sidebar: ## 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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/count.md b/docs/internals/builtins/array/count.md index e265906fbc..1712a148fb 100644 --- a/docs/internals/builtins/array/count.md +++ b/docs/internals/builtins/array/count.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/count.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/count.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/in_array.md b/docs/internals/builtins/array/in_array.md index 59c343fdff..9024214668 100644 --- a/docs/internals/builtins/array/in_array.md +++ b/docs/internals/builtins/array/in_array.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/in_array.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/in_array.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/krsort.md b/docs/internals/builtins/array/krsort.md index 99a1cb71a3..56966aad05 100644 --- a/docs/internals/builtins/array/krsort.md +++ b/docs/internals/builtins/array/krsort.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/krsort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/krsort.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function krsort(array $array): bool +function krsort(array &$array): bool ``` ## What the type checker enforces diff --git a/docs/internals/builtins/array/ksort.md b/docs/internals/builtins/array/ksort.md index 559fd2e180..4fa4ecc58e 100644 --- a/docs/internals/builtins/array/ksort.md +++ b/docs/internals/builtins/array/ksort.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/ksort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/ksort.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function ksort(array $array): bool +function ksort(array &$array): bool ``` ## What the type checker enforces diff --git a/docs/internals/builtins/array/natcasesort.md b/docs/internals/builtins/array/natcasesort.md index ed067a771d..d8b609dbd9 100644 --- a/docs/internals/builtins/array/natcasesort.md +++ b/docs/internals/builtins/array/natcasesort.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/natcasesort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/natcasesort.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function natcasesort(array $array): bool +function natcasesort(array &$array): bool ``` ## What the type checker enforces diff --git a/docs/internals/builtins/array/natsort.md b/docs/internals/builtins/array/natsort.md index bd2ffdc2ba..b8c895eaba 100644 --- a/docs/internals/builtins/array/natsort.md +++ b/docs/internals/builtins/array/natsort.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/natsort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/natsort.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function natsort(array $array): bool +function natsort(array &$array): bool ``` ## What the type checker enforces diff --git a/docs/internals/builtins/array/range.md b/docs/internals/builtins/array/range.md index 2e5fdc86e8..64007bfd90 100644 --- a/docs/internals/builtins/array/range.md +++ b/docs/internals/builtins/array/range.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/range.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/range.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/rsort.md b/docs/internals/builtins/array/rsort.md index dfc4c88797..0f37ee2fdd 100644 --- a/docs/internals/builtins/array/rsort.md +++ b/docs/internals/builtins/array/rsort.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/rsort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/rsort.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function rsort(array $array): bool +function rsort(array &$array): bool ``` ## What the type checker enforces diff --git a/docs/internals/builtins/array/shuffle.md b/docs/internals/builtins/array/shuffle.md index 16a0eef9a0..c0acbfdd25 100644 --- a/docs/internals/builtins/array/shuffle.md +++ b/docs/internals/builtins/array/shuffle.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/shuffle.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/shuffle.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function shuffle(array $array): bool +function shuffle(array &$array): bool ``` ## What the type checker enforces diff --git a/docs/internals/builtins/array/sort.md b/docs/internals/builtins/array/sort.md index 1165ca3f07..24f6e3c7e3 100644 --- a/docs/internals/builtins/array/sort.md +++ b/docs/internals/builtins/array/sort.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/sort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/sort.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function sort(array $array): bool +function sort(array &$array): bool ``` ## What the type checker enforces diff --git a/docs/internals/builtins/array/uasort.md b/docs/internals/builtins/array/uasort.md index 6a3afae267..e283911946 100644 --- a/docs/internals/builtins/array/uasort.md +++ b/docs/internals/builtins/array/uasort.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/uasort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/uasort.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function uasort(array $array, callable $callback): bool +function uasort(array &$array, callable $callback): bool ``` ## What the type checker enforces diff --git a/docs/internals/builtins/array/uksort.md b/docs/internals/builtins/array/uksort.md index 53de407b32..ef3cf55379 100644 --- a/docs/internals/builtins/array/uksort.md +++ b/docs/internals/builtins/array/uksort.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/uksort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/uksort.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function uksort(array $array, callable $callback): bool +function uksort(array &$array, callable $callback): bool ``` ## What the type checker enforces diff --git a/docs/internals/builtins/array/usort.md b/docs/internals/builtins/array/usort.md index 73822cf161..3c6ad0e86a 100644 --- a/docs/internals/builtins/array/usort.md +++ b/docs/internals/builtins/array/usort.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/usort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/usort.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function usort(array $array, callable $callback): bool +function usort(array &$array, callable $callback): bool ``` ## What the type checker enforces diff --git a/docs/internals/builtins/buffer/buffer_free.md b/docs/internals/builtins/buffer/buffer_free.md index 79e77fb4c6..2e6d454fa9 100644 --- a/docs/internals/builtins/buffer/buffer_free.md +++ b/docs/internals/builtins/buffer/buffer_free.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/buffer_free.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/buffer_free.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/buffer/buffer_len.md b/docs/internals/builtins/buffer/buffer_len.md index 986cbf5eaa..ade0b154ee 100644 --- a/docs/internals/builtins/buffer/buffer_len.md +++ b/docs/internals/builtins/buffer/buffer_len.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/buffer_len.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/buffer_len.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/class_alias.md b/docs/internals/builtins/class/class_alias.md index f5528a3d94..689aa400dd 100644 --- a/docs/internals/builtins/class/class_alias.md +++ b/docs/internals/builtins/class/class_alias.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/class_alias.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/class_alias.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/class_attribute_args.md b/docs/internals/builtins/class/class_attribute_args.md index 670acee18c..264f8b2a38 100644 --- a/docs/internals/builtins/class/class_attribute_args.md +++ b/docs/internals/builtins/class/class_attribute_args.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/class_attribute_args.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/class_attribute_args.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/class_attribute_names.md b/docs/internals/builtins/class/class_attribute_names.md index 11c055b1fe..2dac55409c 100644 --- a/docs/internals/builtins/class/class_attribute_names.md +++ b/docs/internals/builtins/class/class_attribute_names.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/class_attribute_names.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/class_attribute_names.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/class_exists.md b/docs/internals/builtins/class/class_exists.md index ce881419d6..f096b1b092 100644 --- a/docs/internals/builtins/class/class_exists.md +++ b/docs/internals/builtins/class/class_exists.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/class_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/class_exists.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/class_get_attributes.md b/docs/internals/builtins/class/class_get_attributes.md index 34e78ff7b9..3086dddd47 100644 --- a/docs/internals/builtins/class/class_get_attributes.md +++ b/docs/internals/builtins/class/class_get_attributes.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/class_get_attributes.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/class_get_attributes.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/class_implements.md b/docs/internals/builtins/class/class_implements.md index 7eba8724c2..f7292a98e5 100644 --- a/docs/internals/builtins/class/class_implements.md +++ b/docs/internals/builtins/class/class_implements.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/class_implements.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/class_implements.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/class_parents.md b/docs/internals/builtins/class/class_parents.md index f7e0a0ae33..c05287b2aa 100644 --- a/docs/internals/builtins/class/class_parents.md +++ b/docs/internals/builtins/class/class_parents.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/class_parents.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/class_parents.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/class_uses.md b/docs/internals/builtins/class/class_uses.md index 6be9cac631..f2f0637161 100644 --- a/docs/internals/builtins/class/class_uses.md +++ b/docs/internals/builtins/class/class_uses.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/class_uses.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/class_uses.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/enum_exists.md b/docs/internals/builtins/class/enum_exists.md index fc3daef966..180a9bbf26 100644 --- a/docs/internals/builtins/class/enum_exists.md +++ b/docs/internals/builtins/class/enum_exists.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/enum_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/enum_exists.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/function_exists.md b/docs/internals/builtins/class/function_exists.md index c9541a4d34..be6f91eea6 100644 --- a/docs/internals/builtins/class/function_exists.md +++ b/docs/internals/builtins/class/function_exists.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/function_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/function_exists.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/get_class.md b/docs/internals/builtins/class/get_class.md index d45a4e7411..4d13254d43 100644 --- a/docs/internals/builtins/class/get_class.md +++ b/docs/internals/builtins/class/get_class.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/get_class.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/get_class.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/get_declared_classes.md b/docs/internals/builtins/class/get_declared_classes.md index c1fb894fb7..328e7232e7 100644 --- a/docs/internals/builtins/class/get_declared_classes.md +++ b/docs/internals/builtins/class/get_declared_classes.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/get_declared_classes.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/get_declared_classes.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/get_declared_interfaces.md b/docs/internals/builtins/class/get_declared_interfaces.md index c7f3d5f7aa..6436396235 100644 --- a/docs/internals/builtins/class/get_declared_interfaces.md +++ b/docs/internals/builtins/class/get_declared_interfaces.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/get_declared_interfaces.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/get_declared_interfaces.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/get_declared_traits.md b/docs/internals/builtins/class/get_declared_traits.md index 2235e6bbf0..525563b068 100644 --- a/docs/internals/builtins/class/get_declared_traits.md +++ b/docs/internals/builtins/class/get_declared_traits.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/get_declared_traits.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/get_declared_traits.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/get_parent_class.md b/docs/internals/builtins/class/get_parent_class.md index a5660976b2..791d2a13e7 100644 --- a/docs/internals/builtins/class/get_parent_class.md +++ b/docs/internals/builtins/class/get_parent_class.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/get_parent_class.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/get_parent_class.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/interface_exists.md b/docs/internals/builtins/class/interface_exists.md index 88bd51d315..5fd1e7300b 100644 --- a/docs/internals/builtins/class/interface_exists.md +++ b/docs/internals/builtins/class/interface_exists.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/interface_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/interface_exists.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/is_a.md b/docs/internals/builtins/class/is_a.md index 16401dc517..8d1c2664b3 100644 --- a/docs/internals/builtins/class/is_a.md +++ b/docs/internals/builtins/class/is_a.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/is_a.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/is_a.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/is_subclass_of.md b/docs/internals/builtins/class/is_subclass_of.md index 3ed8254042..eb5e173f31 100644 --- a/docs/internals/builtins/class/is_subclass_of.md +++ b/docs/internals/builtins/class/is_subclass_of.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/is_subclass_of.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/is_subclass_of.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/method_exists.md b/docs/internals/builtins/class/method_exists.md index bbb0a22e29..7f999529d7 100644 --- a/docs/internals/builtins/class/method_exists.md +++ b/docs/internals/builtins/class/method_exists.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/method_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/method_exists.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/property_exists.md b/docs/internals/builtins/class/property_exists.md index 7d591a791f..2ca5023530 100644 --- a/docs/internals/builtins/class/property_exists.md +++ b/docs/internals/builtins/class/property_exists.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/property_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/property_exists.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/trait_exists.md b/docs/internals/builtins/class/trait_exists.md index b8c6664744..3ab85888c3 100644 --- a/docs/internals/builtins/class/trait_exists.md +++ b/docs/internals/builtins/class/trait_exists.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/trait_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/trait_exists.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/date/checkdate.md b/docs/internals/builtins/date/checkdate.md index 5f3328d14a..c37f2fc6e1 100644 --- a/docs/internals/builtins/date/checkdate.md +++ b/docs/internals/builtins/date/checkdate.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/checkdate.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/checkdate.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/date/date.md b/docs/internals/builtins/date/date.md index 1484776660..946f478486 100644 --- a/docs/internals/builtins/date/date.md +++ b/docs/internals/builtins/date/date.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/date.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/date.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -27,7 +27,7 @@ sidebar: - **Result type source**: `declared` - **Result ownership**: `may_alias_arguments` - **Effects**: `static (16 declared effects)` -- **Requirements**: `static (0 requirements)` +- **Requirements**: `shared` - **Callable policy**: `static_only` - **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` diff --git a/docs/internals/builtins/date/date_default_timezone_get.md b/docs/internals/builtins/date/date_default_timezone_get.md index f77c7fddd4..5aedba1949 100644 --- a/docs/internals/builtins/date/date_default_timezone_get.md +++ b/docs/internals/builtins/date/date_default_timezone_get.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/date/date_default_timezone_set.md b/docs/internals/builtins/date/date_default_timezone_set.md index 1f44b1d55f..9b0d06647f 100644 --- a/docs/internals/builtins/date/date_default_timezone_set.md +++ b/docs/internals/builtins/date/date_default_timezone_set.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/date/getdate.md b/docs/internals/builtins/date/getdate.md index 446f87fc1a..a106902edd 100644 --- a/docs/internals/builtins/date/getdate.md +++ b/docs/internals/builtins/date/getdate.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/getdate.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/getdate.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/date/gmdate.md b/docs/internals/builtins/date/gmdate.md index 32a6104a62..17d15638e1 100644 --- a/docs/internals/builtins/date/gmdate.md +++ b/docs/internals/builtins/date/gmdate.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/gmdate.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/gmdate.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -27,7 +27,7 @@ sidebar: - **Result type source**: `declared` - **Result ownership**: `may_alias_arguments` - **Effects**: `static (16 declared effects)` -- **Requirements**: `static (0 requirements)` +- **Requirements**: `shared` - **Callable policy**: `static_only` - **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` diff --git a/docs/internals/builtins/date/gmmktime.md b/docs/internals/builtins/date/gmmktime.md index 1b0a1fcd06..88559454c1 100644 --- a/docs/internals/builtins/date/gmmktime.md +++ b/docs/internals/builtins/date/gmmktime.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/gmmktime.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/gmmktime.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -27,7 +27,7 @@ sidebar: - **Result type source**: `declared` - **Result ownership**: `may_alias_arguments` - **Effects**: `static (16 declared effects)` -- **Requirements**: `static (0 requirements)` +- **Requirements**: `shared` - **Callable policy**: `static_only` - **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` diff --git a/docs/internals/builtins/date/hrtime.md b/docs/internals/builtins/date/hrtime.md index 1cb9eda64e..e8c07856e9 100644 --- a/docs/internals/builtins/date/hrtime.md +++ b/docs/internals/builtins/date/hrtime.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/hrtime.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/hrtime.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/date/localtime.md b/docs/internals/builtins/date/localtime.md index 88d20049b6..d00fc285d2 100644 --- a/docs/internals/builtins/date/localtime.md +++ b/docs/internals/builtins/date/localtime.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/localtime.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/localtime.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/date/microtime.md b/docs/internals/builtins/date/microtime.md index 044bf24a76..9b10afca6d 100644 --- a/docs/internals/builtins/date/microtime.md +++ b/docs/internals/builtins/date/microtime.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/microtime.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/microtime.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/date/mktime.md b/docs/internals/builtins/date/mktime.md index 09d1f512bb..923d4263a0 100644 --- a/docs/internals/builtins/date/mktime.md +++ b/docs/internals/builtins/date/mktime.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/mktime.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/mktime.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -27,7 +27,7 @@ sidebar: - **Result type source**: `declared` - **Result ownership**: `may_alias_arguments` - **Effects**: `static (16 declared effects)` -- **Requirements**: `static (0 requirements)` +- **Requirements**: `shared` - **Callable policy**: `static_only` - **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` diff --git a/docs/internals/builtins/date/strtotime.md b/docs/internals/builtins/date/strtotime.md index d4f5c1c7df..e1bf1c80bb 100644 --- a/docs/internals/builtins/date/strtotime.md +++ b/docs/internals/builtins/date/strtotime.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/strtotime.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/strtotime.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -27,7 +27,7 @@ sidebar: - **Result type source**: `checked` - **Result ownership**: `may_alias_arguments` - **Effects**: `static (16 declared effects)` -- **Requirements**: `static (0 requirements)` +- **Requirements**: `shared` - **Callable policy**: `static_only` - **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` diff --git a/docs/internals/builtins/date/time.md b/docs/internals/builtins/date/time.md index 5db9cb4145..d575d8a6c9 100644 --- a/docs/internals/builtins/date/time.md +++ b/docs/internals/builtins/date/time.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/time.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/time.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/basename.md b/docs/internals/builtins/filesystem/basename.md index 4c44e64b88..fe7c102d85 100644 --- a/docs/internals/builtins/filesystem/basename.md +++ b/docs/internals/builtins/filesystem/basename.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/basename.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/basename.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/chdir.md b/docs/internals/builtins/filesystem/chdir.md index 22aafa744a..55019a1c39 100644 --- a/docs/internals/builtins/filesystem/chdir.md +++ b/docs/internals/builtins/filesystem/chdir.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/chdir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/chdir.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/chgrp.md b/docs/internals/builtins/filesystem/chgrp.md index 425d561e57..7094d8d3a1 100644 --- a/docs/internals/builtins/filesystem/chgrp.md +++ b/docs/internals/builtins/filesystem/chgrp.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/chgrp.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/chgrp.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function chgrp(string $filename, string $group): bool +function chgrp(string $filename, mixed $group): bool ``` ## What the type checker enforces diff --git a/docs/internals/builtins/filesystem/chmod.md b/docs/internals/builtins/filesystem/chmod.md index 948c79bd5d..ac9f9d568b 100644 --- a/docs/internals/builtins/filesystem/chmod.md +++ b/docs/internals/builtins/filesystem/chmod.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/chmod.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/chmod.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/chown.md b/docs/internals/builtins/filesystem/chown.md index a7708531f3..5b5b5f0ac3 100644 --- a/docs/internals/builtins/filesystem/chown.md +++ b/docs/internals/builtins/filesystem/chown.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/chown.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/chown.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function chown(string $filename, string $user): bool +function chown(string $filename, mixed $user): bool ``` ## What the type checker enforces diff --git a/docs/internals/builtins/filesystem/clearstatcache.md b/docs/internals/builtins/filesystem/clearstatcache.md index 38d1e9726c..7c64e1161a 100644 --- a/docs/internals/builtins/filesystem/clearstatcache.md +++ b/docs/internals/builtins/filesystem/clearstatcache.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/clearstatcache.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/clearstatcache.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/copy.md b/docs/internals/builtins/filesystem/copy.md index 4ea25d121b..f3497372fa 100644 --- a/docs/internals/builtins/filesystem/copy.md +++ b/docs/internals/builtins/filesystem/copy.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/copy.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/copy.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/dirname.md b/docs/internals/builtins/filesystem/dirname.md index 453170840f..aeda61742b 100644 --- a/docs/internals/builtins/filesystem/dirname.md +++ b/docs/internals/builtins/filesystem/dirname.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/dirname.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/dirname.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/disk_free_space.md b/docs/internals/builtins/filesystem/disk_free_space.md index f761e66695..54c0f03ef0 100644 --- a/docs/internals/builtins/filesystem/disk_free_space.md +++ b/docs/internals/builtins/filesystem/disk_free_space.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/disk_free_space.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/disk_free_space.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/disk_total_space.md b/docs/internals/builtins/filesystem/disk_total_space.md index 537d248c8a..4f49e2358d 100644 --- a/docs/internals/builtins/filesystem/disk_total_space.md +++ b/docs/internals/builtins/filesystem/disk_total_space.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/disk_total_space.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/disk_total_space.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/file_exists.md b/docs/internals/builtins/filesystem/file_exists.md index 7558dcde20..19abff486f 100644 --- a/docs/internals/builtins/filesystem/file_exists.md +++ b/docs/internals/builtins/filesystem/file_exists.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/file_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/file_exists.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/fileatime.md b/docs/internals/builtins/filesystem/fileatime.md index 13b65c8e52..9672772251 100644 --- a/docs/internals/builtins/filesystem/fileatime.md +++ b/docs/internals/builtins/filesystem/fileatime.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fileatime.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fileatime.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/filectime.md b/docs/internals/builtins/filesystem/filectime.md index c3b6db4bbd..a3f1dceda2 100644 --- a/docs/internals/builtins/filesystem/filectime.md +++ b/docs/internals/builtins/filesystem/filectime.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/filectime.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/filectime.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/filegroup.md b/docs/internals/builtins/filesystem/filegroup.md index f656adcf79..6317728f4b 100644 --- a/docs/internals/builtins/filesystem/filegroup.md +++ b/docs/internals/builtins/filesystem/filegroup.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/filegroup.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/filegroup.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/fileinode.md b/docs/internals/builtins/filesystem/fileinode.md index 2f10c2ac5a..fda438158e 100644 --- a/docs/internals/builtins/filesystem/fileinode.md +++ b/docs/internals/builtins/filesystem/fileinode.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fileinode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fileinode.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/filemtime.md b/docs/internals/builtins/filesystem/filemtime.md index 776bd740a6..177755c303 100644 --- a/docs/internals/builtins/filesystem/filemtime.md +++ b/docs/internals/builtins/filesystem/filemtime.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/filemtime.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/filemtime.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function filemtime(string $filename): int +function filemtime(string $filename): mixed ``` ## What the type checker enforces diff --git a/docs/internals/builtins/filesystem/fileowner.md b/docs/internals/builtins/filesystem/fileowner.md index e40b8df8ee..0c6d0d36cd 100644 --- a/docs/internals/builtins/filesystem/fileowner.md +++ b/docs/internals/builtins/filesystem/fileowner.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fileowner.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fileowner.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/fileperms.md b/docs/internals/builtins/filesystem/fileperms.md index f4de4158bc..f2e4006071 100644 --- a/docs/internals/builtins/filesystem/fileperms.md +++ b/docs/internals/builtins/filesystem/fileperms.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fileperms.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fileperms.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/filesize.md b/docs/internals/builtins/filesystem/filesize.md index 0f1f30ea81..25105d3d55 100644 --- a/docs/internals/builtins/filesystem/filesize.md +++ b/docs/internals/builtins/filesystem/filesize.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/filesize.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/filesize.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function filesize(string $filename): int +function filesize(string $filename): mixed ``` ## What the type checker enforces diff --git a/docs/internals/builtins/filesystem/filetype.md b/docs/internals/builtins/filesystem/filetype.md index 0fd6afe666..4c11122c7d 100644 --- a/docs/internals/builtins/filesystem/filetype.md +++ b/docs/internals/builtins/filesystem/filetype.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/filetype.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/filetype.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/fnmatch.md b/docs/internals/builtins/filesystem/fnmatch.md index 1f9835bd92..7a8dd4e95b 100644 --- a/docs/internals/builtins/filesystem/fnmatch.md +++ b/docs/internals/builtins/filesystem/fnmatch.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fnmatch.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fnmatch.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/getcwd.md b/docs/internals/builtins/filesystem/getcwd.md index f752a25d3d..0b240d6287 100644 --- a/docs/internals/builtins/filesystem/getcwd.md +++ b/docs/internals/builtins/filesystem/getcwd.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/getcwd.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/getcwd.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/getenv.md b/docs/internals/builtins/filesystem/getenv.md index 1f6a311f75..e951633a4b 100644 --- a/docs/internals/builtins/filesystem/getenv.md +++ b/docs/internals/builtins/filesystem/getenv.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/getenv.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/getenv.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/glob.md b/docs/internals/builtins/filesystem/glob.md index 189b5e28f6..db244b0af5 100644 --- a/docs/internals/builtins/filesystem/glob.md +++ b/docs/internals/builtins/filesystem/glob.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/glob.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/glob.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/is_dir.md b/docs/internals/builtins/filesystem/is_dir.md index 254a3f9cec..0d4a148f31 100644 --- a/docs/internals/builtins/filesystem/is_dir.md +++ b/docs/internals/builtins/filesystem/is_dir.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/is_dir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/is_dir.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/is_executable.md b/docs/internals/builtins/filesystem/is_executable.md index 283609f22f..b1c4ada117 100644 --- a/docs/internals/builtins/filesystem/is_executable.md +++ b/docs/internals/builtins/filesystem/is_executable.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/is_executable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/is_executable.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/is_file.md b/docs/internals/builtins/filesystem/is_file.md index cb1788ed37..8fc855aff1 100644 --- a/docs/internals/builtins/filesystem/is_file.md +++ b/docs/internals/builtins/filesystem/is_file.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/is_file.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/is_file.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/is_link.md b/docs/internals/builtins/filesystem/is_link.md index 51d82e32c8..0aa49531f8 100644 --- a/docs/internals/builtins/filesystem/is_link.md +++ b/docs/internals/builtins/filesystem/is_link.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/is_link.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/is_link.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/is_readable.md b/docs/internals/builtins/filesystem/is_readable.md index 6ea5aa8c4f..ada14736e4 100644 --- a/docs/internals/builtins/filesystem/is_readable.md +++ b/docs/internals/builtins/filesystem/is_readable.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/is_readable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/is_readable.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/is_writable.md b/docs/internals/builtins/filesystem/is_writable.md index e310b78bca..9b5c08ec30 100644 --- a/docs/internals/builtins/filesystem/is_writable.md +++ b/docs/internals/builtins/filesystem/is_writable.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/is_writable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/is_writable.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/is_writeable.md b/docs/internals/builtins/filesystem/is_writeable.md index 9a6975332f..0f7abc9e71 100644 --- a/docs/internals/builtins/filesystem/is_writeable.md +++ b/docs/internals/builtins/filesystem/is_writeable.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/is_writeable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/is_writeable.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/lchgrp.md b/docs/internals/builtins/filesystem/lchgrp.md index 0846c9f65b..b8634bbceb 100644 --- a/docs/internals/builtins/filesystem/lchgrp.md +++ b/docs/internals/builtins/filesystem/lchgrp.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/lchgrp.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/lchgrp.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function lchgrp(string $filename, string $group): bool +function lchgrp(string $filename, mixed $group): bool ``` ## What the type checker enforces diff --git a/docs/internals/builtins/filesystem/lchown.md b/docs/internals/builtins/filesystem/lchown.md index 23170e54db..041793fe27 100644 --- a/docs/internals/builtins/filesystem/lchown.md +++ b/docs/internals/builtins/filesystem/lchown.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/lchown.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/lchown.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function lchown(string $filename, string $user): bool +function lchown(string $filename, mixed $user): bool ``` ## What the type checker enforces diff --git a/docs/internals/builtins/filesystem/link.md b/docs/internals/builtins/filesystem/link.md index f532cdf885..20e659587a 100644 --- a/docs/internals/builtins/filesystem/link.md +++ b/docs/internals/builtins/filesystem/link.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/link.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/link.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/linkinfo.md b/docs/internals/builtins/filesystem/linkinfo.md index 693fcfaa37..2f496a7a77 100644 --- a/docs/internals/builtins/filesystem/linkinfo.md +++ b/docs/internals/builtins/filesystem/linkinfo.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/linkinfo.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/linkinfo.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/lstat.md b/docs/internals/builtins/filesystem/lstat.md index 6f99b6ab41..fc6f3e7af5 100644 --- a/docs/internals/builtins/filesystem/lstat.md +++ b/docs/internals/builtins/filesystem/lstat.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/lstat.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/lstat.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/mkdir.md b/docs/internals/builtins/filesystem/mkdir.md index d3496bcb81..27726e3cf2 100644 --- a/docs/internals/builtins/filesystem/mkdir.md +++ b/docs/internals/builtins/filesystem/mkdir.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/mkdir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/mkdir.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/pathinfo.md b/docs/internals/builtins/filesystem/pathinfo.md index 6892e30f2f..369d377e81 100644 --- a/docs/internals/builtins/filesystem/pathinfo.md +++ b/docs/internals/builtins/filesystem/pathinfo.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/pathinfo.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/pathinfo.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/putenv.md b/docs/internals/builtins/filesystem/putenv.md index 472a001c54..a6e1f3597b 100644 --- a/docs/internals/builtins/filesystem/putenv.md +++ b/docs/internals/builtins/filesystem/putenv.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/putenv.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/putenv.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/readfile.md b/docs/internals/builtins/filesystem/readfile.md index b66b8d7a0e..61f8d33814 100644 --- a/docs/internals/builtins/filesystem/readfile.md +++ b/docs/internals/builtins/filesystem/readfile.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/readfile.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/readfile.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/readlink.md b/docs/internals/builtins/filesystem/readlink.md index d5169ecc8c..6303290b15 100644 --- a/docs/internals/builtins/filesystem/readlink.md +++ b/docs/internals/builtins/filesystem/readlink.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/readlink.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/readlink.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/realpath.md b/docs/internals/builtins/filesystem/realpath.md index f68773d38e..756b2ae5f3 100644 --- a/docs/internals/builtins/filesystem/realpath.md +++ b/docs/internals/builtins/filesystem/realpath.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/realpath.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/realpath.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/realpath_cache_get.md b/docs/internals/builtins/filesystem/realpath_cache_get.md index 5976b59a00..67f224a54e 100644 --- a/docs/internals/builtins/filesystem/realpath_cache_get.md +++ b/docs/internals/builtins/filesystem/realpath_cache_get.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/realpath_cache_get.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/realpath_cache_get.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/realpath_cache_size.md b/docs/internals/builtins/filesystem/realpath_cache_size.md index cc262c8e7d..2eaeba3aff 100644 --- a/docs/internals/builtins/filesystem/realpath_cache_size.md +++ b/docs/internals/builtins/filesystem/realpath_cache_size.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/realpath_cache_size.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/realpath_cache_size.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/rename.md b/docs/internals/builtins/filesystem/rename.md index bd2b1cad86..031e6b27a3 100644 --- a/docs/internals/builtins/filesystem/rename.md +++ b/docs/internals/builtins/filesystem/rename.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/rename.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/rename.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/rmdir.md b/docs/internals/builtins/filesystem/rmdir.md index 1bac4f7142..53920c3767 100644 --- a/docs/internals/builtins/filesystem/rmdir.md +++ b/docs/internals/builtins/filesystem/rmdir.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/rmdir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/rmdir.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/scandir.md b/docs/internals/builtins/filesystem/scandir.md index 1d29c7e79d..309e4a89f0 100644 --- a/docs/internals/builtins/filesystem/scandir.md +++ b/docs/internals/builtins/filesystem/scandir.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/scandir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/scandir.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/stat.md b/docs/internals/builtins/filesystem/stat.md index 1776bededa..3ab5deabf8 100644 --- a/docs/internals/builtins/filesystem/stat.md +++ b/docs/internals/builtins/filesystem/stat.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stat.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stat.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/symlink.md b/docs/internals/builtins/filesystem/symlink.md index eb51274b9a..2ff624a105 100644 --- a/docs/internals/builtins/filesystem/symlink.md +++ b/docs/internals/builtins/filesystem/symlink.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/symlink.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/symlink.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/sys_get_temp_dir.md b/docs/internals/builtins/filesystem/sys_get_temp_dir.md index 83bab9c1ef..658cadb983 100644 --- a/docs/internals/builtins/filesystem/sys_get_temp_dir.md +++ b/docs/internals/builtins/filesystem/sys_get_temp_dir.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/tempnam.md b/docs/internals/builtins/filesystem/tempnam.md index 9e7d9802ff..ebaaf49fe1 100644 --- a/docs/internals/builtins/filesystem/tempnam.md +++ b/docs/internals/builtins/filesystem/tempnam.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/tempnam.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/tempnam.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -23,8 +23,8 @@ sidebar: ## Semantic descriptor - **Target strategy**: `runtime_call` -- **Validation**: `signature` -- **Result type source**: `declared` +- **Validation**: `checker_hook` +- **Result type source**: `checked` - **Result ownership**: `may_alias_arguments` - **Effects**: `static (16 declared effects)` - **Requirements**: `static (0 requirements)` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function tempnam(string $directory, string $prefix): string +function tempnam(string $directory, string $prefix): mixed ``` ## What the type checker enforces diff --git a/docs/internals/builtins/filesystem/tmpfile.md b/docs/internals/builtins/filesystem/tmpfile.md index d6864cb2a5..a356264a30 100644 --- a/docs/internals/builtins/filesystem/tmpfile.md +++ b/docs/internals/builtins/filesystem/tmpfile.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/tmpfile.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/tmpfile.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/touch.md b/docs/internals/builtins/filesystem/touch.md index 4cec86705f..c51052d067 100644 --- a/docs/internals/builtins/filesystem/touch.md +++ b/docs/internals/builtins/filesystem/touch.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/touch.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/touch.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/umask.md b/docs/internals/builtins/filesystem/umask.md index b8ef43b340..ea14ab8d0a 100644 --- a/docs/internals/builtins/filesystem/umask.md +++ b/docs/internals/builtins/filesystem/umask.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/umask.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/umask.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/unlink.md b/docs/internals/builtins/filesystem/unlink.md index 4e90c4a616..473cebd9ea 100644 --- a/docs/internals/builtins/filesystem/unlink.md +++ b/docs/internals/builtins/filesystem/unlink.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/unlink.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/unlink.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/closedir.md b/docs/internals/builtins/io/closedir.md index 89234418a7..a0a1036587 100644 --- a/docs/internals/builtins/io/closedir.md +++ b/docs/internals/builtins/io/closedir.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/closedir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/closedir.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fclose.md b/docs/internals/builtins/io/fclose.md index e7679d4f56..b36ea970d1 100644 --- a/docs/internals/builtins/io/fclose.md +++ b/docs/internals/builtins/io/fclose.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fclose.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fclose.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fdatasync.md b/docs/internals/builtins/io/fdatasync.md index 50949b4a96..6dfea71d81 100644 --- a/docs/internals/builtins/io/fdatasync.md +++ b/docs/internals/builtins/io/fdatasync.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fdatasync.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fdatasync.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/feof.md b/docs/internals/builtins/io/feof.md index 3c7505d11f..785df2edf6 100644 --- a/docs/internals/builtins/io/feof.md +++ b/docs/internals/builtins/io/feof.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/feof.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/feof.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fflush.md b/docs/internals/builtins/io/fflush.md index b183808bce..46792386d2 100644 --- a/docs/internals/builtins/io/fflush.md +++ b/docs/internals/builtins/io/fflush.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fflush.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fflush.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fgetc.md b/docs/internals/builtins/io/fgetc.md index f534ba7889..e3092eda48 100644 --- a/docs/internals/builtins/io/fgetc.md +++ b/docs/internals/builtins/io/fgetc.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fgetc.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fgetc.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fgetcsv.md b/docs/internals/builtins/io/fgetcsv.md index 037d848e12..41d3d42287 100644 --- a/docs/internals/builtins/io/fgetcsv.md +++ b/docs/internals/builtins/io/fgetcsv.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fgetcsv.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fgetcsv.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fgets.md b/docs/internals/builtins/io/fgets.md index dc143309ba..57ef453bba 100644 --- a/docs/internals/builtins/io/fgets.md +++ b/docs/internals/builtins/io/fgets.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fgets.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fgets.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/file.md b/docs/internals/builtins/io/file.md index 57bc2bd3d7..dc652ae055 100644 --- a/docs/internals/builtins/io/file.md +++ b/docs/internals/builtins/io/file.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/file.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/file.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/file_get_contents.md b/docs/internals/builtins/io/file_get_contents.md index 4b69fdfd27..2966efab5a 100644 --- a/docs/internals/builtins/io/file_get_contents.md +++ b/docs/internals/builtins/io/file_get_contents.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/file_get_contents.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/file_get_contents.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/file_put_contents.md b/docs/internals/builtins/io/file_put_contents.md index 322570a1a5..f3e4cff763 100644 --- a/docs/internals/builtins/io/file_put_contents.md +++ b/docs/internals/builtins/io/file_put_contents.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/file_put_contents.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/file_put_contents.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/flock.md b/docs/internals/builtins/io/flock.md index c8d7a6a786..ce1fb423dc 100644 --- a/docs/internals/builtins/io/flock.md +++ b/docs/internals/builtins/io/flock.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/flock.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/flock.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function flock(resource $stream, int $operation, bool $would_block = null): bool +function flock(resource $stream, int $operation, bool &$would_block = null): bool ``` ## What the type checker enforces diff --git a/docs/internals/builtins/io/fopen.md b/docs/internals/builtins/io/fopen.md index eb88666601..1dca50e587 100644 --- a/docs/internals/builtins/io/fopen.md +++ b/docs/internals/builtins/io/fopen.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fopen.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fopen.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fpassthru.md b/docs/internals/builtins/io/fpassthru.md index 0eed7032a9..c6ee7c1ae0 100644 --- a/docs/internals/builtins/io/fpassthru.md +++ b/docs/internals/builtins/io/fpassthru.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fpassthru.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fpassthru.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fprintf.md b/docs/internals/builtins/io/fprintf.md index 10ae07077f..57976b891e 100644 --- a/docs/internals/builtins/io/fprintf.md +++ b/docs/internals/builtins/io/fprintf.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fprintf.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fputcsv.md b/docs/internals/builtins/io/fputcsv.md index 0989382a25..7c744afe72 100644 --- a/docs/internals/builtins/io/fputcsv.md +++ b/docs/internals/builtins/io/fputcsv.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fputcsv.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fputcsv.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fread.md b/docs/internals/builtins/io/fread.md index de7f850cbf..c3a2ab607e 100644 --- a/docs/internals/builtins/io/fread.md +++ b/docs/internals/builtins/io/fread.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fread.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fread.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fscanf.md b/docs/internals/builtins/io/fscanf.md index d2284fe8fe..736760622e 100644 --- a/docs/internals/builtins/io/fscanf.md +++ b/docs/internals/builtins/io/fscanf.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fscanf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fscanf.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fseek.md b/docs/internals/builtins/io/fseek.md index d83ea2d941..848cf20cf1 100644 --- a/docs/internals/builtins/io/fseek.md +++ b/docs/internals/builtins/io/fseek.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fseek.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fseek.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fstat.md b/docs/internals/builtins/io/fstat.md index 77db2531ad..a83b3d6439 100644 --- a/docs/internals/builtins/io/fstat.md +++ b/docs/internals/builtins/io/fstat.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fstat.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fstat.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fsync.md b/docs/internals/builtins/io/fsync.md index 3b04e97355..5d76935706 100644 --- a/docs/internals/builtins/io/fsync.md +++ b/docs/internals/builtins/io/fsync.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fsync.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fsync.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ftell.md b/docs/internals/builtins/io/ftell.md index 3ed53617f0..de4e8462c5 100644 --- a/docs/internals/builtins/io/ftell.md +++ b/docs/internals/builtins/io/ftell.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ftell.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ftell.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ftruncate.md b/docs/internals/builtins/io/ftruncate.md index 3c43448701..9291bb63f5 100644 --- a/docs/internals/builtins/io/ftruncate.md +++ b/docs/internals/builtins/io/ftruncate.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ftruncate.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ftruncate.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fwrite.md b/docs/internals/builtins/io/fwrite.md index dd5808cf7b..4479b77678 100644 --- a/docs/internals/builtins/io/fwrite.md +++ b/docs/internals/builtins/io/fwrite.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fwrite.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fwrite.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/gethostbyaddr.md b/docs/internals/builtins/io/gethostbyaddr.md index 7233109c78..28b03f0b47 100644 --- a/docs/internals/builtins/io/gethostbyaddr.md +++ b/docs/internals/builtins/io/gethostbyaddr.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/gethostbyaddr.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/gethostbyaddr.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/gethostbyname.md b/docs/internals/builtins/io/gethostbyname.md index 655fd1e22f..1bb405b6ed 100644 --- a/docs/internals/builtins/io/gethostbyname.md +++ b/docs/internals/builtins/io/gethostbyname.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/gethostbyname.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/gethostbyname.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/gethostname.md b/docs/internals/builtins/io/gethostname.md index 3cd3898732..d833bba6b2 100644 --- a/docs/internals/builtins/io/gethostname.md +++ b/docs/internals/builtins/io/gethostname.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/gethostname.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/gethostname.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/getprotobyname.md b/docs/internals/builtins/io/getprotobyname.md index 44f76ba715..3ca533e8c1 100644 --- a/docs/internals/builtins/io/getprotobyname.md +++ b/docs/internals/builtins/io/getprotobyname.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/getprotobyname.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/getprotobyname.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/getprotobynumber.md b/docs/internals/builtins/io/getprotobynumber.md index b85c084bea..380d19cc67 100644 --- a/docs/internals/builtins/io/getprotobynumber.md +++ b/docs/internals/builtins/io/getprotobynumber.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/getprotobynumber.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/getprotobynumber.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/getservbyname.md b/docs/internals/builtins/io/getservbyname.md index ec04ce51c1..419ff6b5e1 100644 --- a/docs/internals/builtins/io/getservbyname.md +++ b/docs/internals/builtins/io/getservbyname.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/getservbyname.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/getservbyname.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/getservbyport.md b/docs/internals/builtins/io/getservbyport.md index f3e22f3b48..a68996c1fe 100644 --- a/docs/internals/builtins/io/getservbyport.md +++ b/docs/internals/builtins/io/getservbyport.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/getservbyport.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/getservbyport.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/hash_file.md b/docs/internals/builtins/io/hash_file.md index 14205b0694..a1c2449c07 100644 --- a/docs/internals/builtins/io/hash_file.md +++ b/docs/internals/builtins/io/hash_file.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/hash_file.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/hash_file.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ob_clean.md b/docs/internals/builtins/io/ob_clean.md index 31f3ade572..7d5b07aff2 100644 --- a/docs/internals/builtins/io/ob_clean.md +++ b/docs/internals/builtins/io/ob_clean.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ob_clean.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ob_clean.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ob_end_clean.md b/docs/internals/builtins/io/ob_end_clean.md index a5a1dc385c..1aa5e062b4 100644 --- a/docs/internals/builtins/io/ob_end_clean.md +++ b/docs/internals/builtins/io/ob_end_clean.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ob_end_clean.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ob_end_clean.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ob_end_flush.md b/docs/internals/builtins/io/ob_end_flush.md index 35f1a0be9b..5523f92f49 100644 --- a/docs/internals/builtins/io/ob_end_flush.md +++ b/docs/internals/builtins/io/ob_end_flush.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ob_end_flush.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ob_end_flush.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ob_flush.md b/docs/internals/builtins/io/ob_flush.md index f2c9179908..c2dfba0e6b 100644 --- a/docs/internals/builtins/io/ob_flush.md +++ b/docs/internals/builtins/io/ob_flush.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ob_flush.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ob_flush.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ob_get_clean.md b/docs/internals/builtins/io/ob_get_clean.md index 19b657e0aa..2838a8250e 100644 --- a/docs/internals/builtins/io/ob_get_clean.md +++ b/docs/internals/builtins/io/ob_get_clean.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ob_get_clean.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ob_get_clean.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ob_get_contents.md b/docs/internals/builtins/io/ob_get_contents.md index 46452e07cb..491cd62fde 100644 --- a/docs/internals/builtins/io/ob_get_contents.md +++ b/docs/internals/builtins/io/ob_get_contents.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ob_get_contents.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ob_get_contents.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ob_get_flush.md b/docs/internals/builtins/io/ob_get_flush.md index 34f4bd1762..d80e11e850 100644 --- a/docs/internals/builtins/io/ob_get_flush.md +++ b/docs/internals/builtins/io/ob_get_flush.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ob_get_flush.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ob_get_flush.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ob_get_length.md b/docs/internals/builtins/io/ob_get_length.md index 0cd50fb0ce..8f15e97bc1 100644 --- a/docs/internals/builtins/io/ob_get_length.md +++ b/docs/internals/builtins/io/ob_get_length.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ob_get_length.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ob_get_length.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ob_get_level.md b/docs/internals/builtins/io/ob_get_level.md index 3ef19463a4..80445439a4 100644 --- a/docs/internals/builtins/io/ob_get_level.md +++ b/docs/internals/builtins/io/ob_get_level.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ob_get_level.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ob_get_level.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ob_get_status.md b/docs/internals/builtins/io/ob_get_status.md index 5889be0e4b..b56d695f8a 100644 --- a/docs/internals/builtins/io/ob_get_status.md +++ b/docs/internals/builtins/io/ob_get_status.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ob_get_status.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ob_get_status.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ob_implicit_flush.md b/docs/internals/builtins/io/ob_implicit_flush.md index f9ecfb9df3..d8d9de91fd 100644 --- a/docs/internals/builtins/io/ob_implicit_flush.md +++ b/docs/internals/builtins/io/ob_implicit_flush.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ob_implicit_flush.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ob_implicit_flush.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ob_list_handlers.md b/docs/internals/builtins/io/ob_list_handlers.md index 70bc0d9e08..bfaea18913 100644 --- a/docs/internals/builtins/io/ob_list_handlers.md +++ b/docs/internals/builtins/io/ob_list_handlers.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ob_list_handlers.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ob_list_handlers.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ob_start.md b/docs/internals/builtins/io/ob_start.md index b0e0a3a6a6..c0143636a3 100644 --- a/docs/internals/builtins/io/ob_start.md +++ b/docs/internals/builtins/io/ob_start.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ob_start.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ob_start.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/opendir.md b/docs/internals/builtins/io/opendir.md index ffe97a2451..0fedcb0dcf 100644 --- a/docs/internals/builtins/io/opendir.md +++ b/docs/internals/builtins/io/opendir.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/opendir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/opendir.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/readdir.md b/docs/internals/builtins/io/readdir.md index 674244f8a6..4d280e3f63 100644 --- a/docs/internals/builtins/io/readdir.md +++ b/docs/internals/builtins/io/readdir.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/readdir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/readdir.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/rewind.md b/docs/internals/builtins/io/rewind.md index 7773d00050..c1a93a44fc 100644 --- a/docs/internals/builtins/io/rewind.md +++ b/docs/internals/builtins/io/rewind.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/rewind.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/rewind.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/rewinddir.md b/docs/internals/builtins/io/rewinddir.md index 018c31e517..d196060bdd 100644 --- a/docs/internals/builtins/io/rewinddir.md +++ b/docs/internals/builtins/io/rewinddir.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/rewinddir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/rewinddir.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_bucket_make_writeable.md b/docs/internals/builtins/io/stream_bucket_make_writeable.md index 1cb2224b1b..10f3399bd8 100644 --- a/docs/internals/builtins/io/stream_bucket_make_writeable.md +++ b/docs/internals/builtins/io/stream_bucket_make_writeable.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_bucket_new.md b/docs/internals/builtins/io/stream_bucket_new.md index 51cfc6010b..b5be83aeff 100644 --- a/docs/internals/builtins/io/stream_bucket_new.md +++ b/docs/internals/builtins/io/stream_bucket_new.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_bucket_new.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_bucket_new.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_context_create.md b/docs/internals/builtins/io/stream_context_create.md index 434eab349d..98d0b38874 100644 --- a/docs/internals/builtins/io/stream_context_create.md +++ b/docs/internals/builtins/io/stream_context_create.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_context_create.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_context_create.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_context_get_default.md b/docs/internals/builtins/io/stream_context_get_default.md index c85376820b..82a6a8230a 100644 --- a/docs/internals/builtins/io/stream_context_get_default.md +++ b/docs/internals/builtins/io/stream_context_get_default.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_context_get_options.md b/docs/internals/builtins/io/stream_context_get_options.md index bb0b9a95f1..edf35e8a0f 100644 --- a/docs/internals/builtins/io/stream_context_get_options.md +++ b/docs/internals/builtins/io/stream_context_get_options.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_context_get_params.md b/docs/internals/builtins/io/stream_context_get_params.md index b4f42b3b9a..c4ad54d936 100644 --- a/docs/internals/builtins/io/stream_context_get_params.md +++ b/docs/internals/builtins/io/stream_context_get_params.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_context_set_default.md b/docs/internals/builtins/io/stream_context_set_default.md index e4a0170ae2..9f65a09d1e 100644 --- a/docs/internals/builtins/io/stream_context_set_default.md +++ b/docs/internals/builtins/io/stream_context_set_default.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_context_set_option.md b/docs/internals/builtins/io/stream_context_set_option.md index 1ac261f401..a1d1230108 100644 --- a/docs/internals/builtins/io/stream_context_set_option.md +++ b/docs/internals/builtins/io/stream_context_set_option.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_context_set_params.md b/docs/internals/builtins/io/stream_context_set_params.md index 164aba6dfe..ad8621346e 100644 --- a/docs/internals/builtins/io/stream_context_set_params.md +++ b/docs/internals/builtins/io/stream_context_set_params.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_copy_to_stream.md b/docs/internals/builtins/io/stream_copy_to_stream.md index 9c2ef59ba8..dd1bd26771 100644 --- a/docs/internals/builtins/io/stream_copy_to_stream.md +++ b/docs/internals/builtins/io/stream_copy_to_stream.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_filter_register.md b/docs/internals/builtins/io/stream_filter_register.md index 8e3c6896b3..a6dc307299 100644 --- a/docs/internals/builtins/io/stream_filter_register.md +++ b/docs/internals/builtins/io/stream_filter_register.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_filter_register.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_filter_register.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_filter_remove.md b/docs/internals/builtins/io/stream_filter_remove.md index 21cd7b51d2..1480969f9d 100644 --- a/docs/internals/builtins/io/stream_filter_remove.md +++ b/docs/internals/builtins/io/stream_filter_remove.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_filter_remove.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_filter_remove.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_get_contents.md b/docs/internals/builtins/io/stream_get_contents.md index ab9fe349b4..9b7167cc5c 100644 --- a/docs/internals/builtins/io/stream_get_contents.md +++ b/docs/internals/builtins/io/stream_get_contents.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_get_contents.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_get_contents.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_get_filters.md b/docs/internals/builtins/io/stream_get_filters.md index 7e2bb5cde2..9cc645276e 100644 --- a/docs/internals/builtins/io/stream_get_filters.md +++ b/docs/internals/builtins/io/stream_get_filters.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_get_filters.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_get_filters.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_get_line.md b/docs/internals/builtins/io/stream_get_line.md index ade5996358..f0156ccf41 100644 --- a/docs/internals/builtins/io/stream_get_line.md +++ b/docs/internals/builtins/io/stream_get_line.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_get_line.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_get_line.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_get_meta_data.md b/docs/internals/builtins/io/stream_get_meta_data.md index 89214df4b0..1b6f59ad08 100644 --- a/docs/internals/builtins/io/stream_get_meta_data.md +++ b/docs/internals/builtins/io/stream_get_meta_data.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_get_transports.md b/docs/internals/builtins/io/stream_get_transports.md index 85db7d0758..57f534fe92 100644 --- a/docs/internals/builtins/io/stream_get_transports.md +++ b/docs/internals/builtins/io/stream_get_transports.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_get_transports.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_get_transports.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_get_wrappers.md b/docs/internals/builtins/io/stream_get_wrappers.md index 47b3566dd2..28dde87e1a 100644 --- a/docs/internals/builtins/io/stream_get_wrappers.md +++ b/docs/internals/builtins/io/stream_get_wrappers.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_get_wrappers.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_get_wrappers.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_is_local.md b/docs/internals/builtins/io/stream_is_local.md index d2ea33e907..e3d7c88697 100644 --- a/docs/internals/builtins/io/stream_is_local.md +++ b/docs/internals/builtins/io/stream_is_local.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_is_local.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_is_local.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_isatty.md b/docs/internals/builtins/io/stream_isatty.md index f952f055ef..348846acb3 100644 --- a/docs/internals/builtins/io/stream_isatty.md +++ b/docs/internals/builtins/io/stream_isatty.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_isatty.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_isatty.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_resolve_include_path.md b/docs/internals/builtins/io/stream_resolve_include_path.md index db2cccf200..4455ad6731 100644 --- a/docs/internals/builtins/io/stream_resolve_include_path.md +++ b/docs/internals/builtins/io/stream_resolve_include_path.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_select.md b/docs/internals/builtins/io/stream_select.md index 8c8b274918..6192bf23d2 100644 --- a/docs/internals/builtins/io/stream_select.md +++ b/docs/internals/builtins/io/stream_select.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_select.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_select.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function stream_select(array $read, array $write, array $except, int $seconds, int $microseconds = 0): int +function stream_select(array &$read, array &$write, array &$except, int $seconds, int $microseconds = 0): int ``` ## What the type checker enforces diff --git a/docs/internals/builtins/io/stream_set_blocking.md b/docs/internals/builtins/io/stream_set_blocking.md index 303150d708..a3e5b34bd1 100644 --- a/docs/internals/builtins/io/stream_set_blocking.md +++ b/docs/internals/builtins/io/stream_set_blocking.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_set_blocking.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_set_blocking.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_set_chunk_size.md b/docs/internals/builtins/io/stream_set_chunk_size.md index d1180c6d3a..da1106a775 100644 --- a/docs/internals/builtins/io/stream_set_chunk_size.md +++ b/docs/internals/builtins/io/stream_set_chunk_size.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_set_read_buffer.md b/docs/internals/builtins/io/stream_set_read_buffer.md index bbd112184c..be00679bd4 100644 --- a/docs/internals/builtins/io/stream_set_read_buffer.md +++ b/docs/internals/builtins/io/stream_set_read_buffer.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_set_timeout.md b/docs/internals/builtins/io/stream_set_timeout.md index b47a13d7f0..b17daac2ff 100644 --- a/docs/internals/builtins/io/stream_set_timeout.md +++ b/docs/internals/builtins/io/stream_set_timeout.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_set_timeout.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_set_timeout.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_set_write_buffer.md b/docs/internals/builtins/io/stream_set_write_buffer.md index c18b669f9a..97990b1efb 100644 --- a/docs/internals/builtins/io/stream_set_write_buffer.md +++ b/docs/internals/builtins/io/stream_set_write_buffer.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_socket_accept.md b/docs/internals/builtins/io/stream_socket_accept.md index fbc4496d47..7c171f9790 100644 --- a/docs/internals/builtins/io/stream_socket_accept.md +++ b/docs/internals/builtins/io/stream_socket_accept.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_socket_accept.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_socket_accept.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function stream_socket_accept(resource $socket, float $timeout = null, string $peer_name = null): mixed +function stream_socket_accept(resource $socket, float $timeout = null, string &$peer_name = null): mixed ``` ## What the type checker enforces diff --git a/docs/internals/builtins/io/stream_socket_client.md b/docs/internals/builtins/io/stream_socket_client.md index f760c86981..ceace88d00 100644 --- a/docs/internals/builtins/io/stream_socket_client.md +++ b/docs/internals/builtins/io/stream_socket_client.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_socket_client.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_socket_client.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -27,7 +27,7 @@ sidebar: - **Result type source**: `checked` - **Result ownership**: `may_alias_arguments` - **Effects**: `static (16 declared effects)` -- **Requirements**: `static (0 requirements)` +- **Requirements**: `shared` - **Callable policy**: `static_only` - **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` diff --git a/docs/internals/builtins/io/stream_socket_enable_crypto.md b/docs/internals/builtins/io/stream_socket_enable_crypto.md index 7e446a9d87..8fdf00abdf 100644 --- a/docs/internals/builtins/io/stream_socket_enable_crypto.md +++ b/docs/internals/builtins/io/stream_socket_enable_crypto.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function stream_socket_enable_crypto(resource $stream, bool $enable, int $crypto_method = null, resource $session_stream = null): bool +function stream_socket_enable_crypto(resource $stream, bool $enable, int $crypto_method = null, resource $session_stream = null): mixed ``` ## What the type checker enforces diff --git a/docs/internals/builtins/io/stream_socket_get_name.md b/docs/internals/builtins/io/stream_socket_get_name.md index 1cab54ef6e..46d5c30eb8 100644 --- a/docs/internals/builtins/io/stream_socket_get_name.md +++ b/docs/internals/builtins/io/stream_socket_get_name.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_socket_pair.md b/docs/internals/builtins/io/stream_socket_pair.md index 2770a14914..7c62099825 100644 --- a/docs/internals/builtins/io/stream_socket_pair.md +++ b/docs/internals/builtins/io/stream_socket_pair.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_socket_pair.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_socket_pair.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_socket_recvfrom.md b/docs/internals/builtins/io/stream_socket_recvfrom.md index 2c0379bf04..4cda5ffe06 100644 --- a/docs/internals/builtins/io/stream_socket_recvfrom.md +++ b/docs/internals/builtins/io/stream_socket_recvfrom.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_socket_recvfrom.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_socket_recvfrom.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function stream_socket_recvfrom(resource $socket, int $length, int $flags = 0, string $address = ''): mixed +function stream_socket_recvfrom(resource $socket, int $length, int $flags = 0, string &$address = ''): mixed ``` ## What the type checker enforces diff --git a/docs/internals/builtins/io/stream_socket_sendto.md b/docs/internals/builtins/io/stream_socket_sendto.md index 5d90e85768..a089e68f34 100644 --- a/docs/internals/builtins/io/stream_socket_sendto.md +++ b/docs/internals/builtins/io/stream_socket_sendto.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_socket_sendto.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_socket_sendto.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_socket_server.md b/docs/internals/builtins/io/stream_socket_server.md index b437a9bc09..799c524db5 100644 --- a/docs/internals/builtins/io/stream_socket_server.md +++ b/docs/internals/builtins/io/stream_socket_server.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_socket_server.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_socket_server.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_socket_shutdown.md b/docs/internals/builtins/io/stream_socket_shutdown.md index a8e91d570a..ec12fa6dfe 100644 --- a/docs/internals/builtins/io/stream_socket_shutdown.md +++ b/docs/internals/builtins/io/stream_socket_shutdown.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_socket_shutdown.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_socket_shutdown.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_supports_lock.md b/docs/internals/builtins/io/stream_supports_lock.md index 24914086ca..d2f5072b61 100644 --- a/docs/internals/builtins/io/stream_supports_lock.md +++ b/docs/internals/builtins/io/stream_supports_lock.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_supports_lock.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_supports_lock.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_wrapper_register.md b/docs/internals/builtins/io/stream_wrapper_register.md index 034f3c477f..adb734208c 100644 --- a/docs/internals/builtins/io/stream_wrapper_register.md +++ b/docs/internals/builtins/io/stream_wrapper_register.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_wrapper_register.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_wrapper_register.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_wrapper_restore.md b/docs/internals/builtins/io/stream_wrapper_restore.md index dc3049bafe..eb41bcb20e 100644 --- a/docs/internals/builtins/io/stream_wrapper_restore.md +++ b/docs/internals/builtins/io/stream_wrapper_restore.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_wrapper_restore.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_wrapper_restore.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_wrapper_unregister.md b/docs/internals/builtins/io/stream_wrapper_unregister.md index d6e256880d..13e500e5db 100644 --- a/docs/internals/builtins/io/stream_wrapper_unregister.md +++ b/docs/internals/builtins/io/stream_wrapper_unregister.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_wrapper_unregister.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_wrapper_unregister.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/vfprintf.md b/docs/internals/builtins/io/vfprintf.md index 85e7005d47..d35df9116b 100644 --- a/docs/internals/builtins/io/vfprintf.md +++ b/docs/internals/builtins/io/vfprintf.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/vfprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/vfprintf.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/json/json_decode.md b/docs/internals/builtins/json/json_decode.md index 58b8688b69..7bd2a07450 100644 --- a/docs/internals/builtins/json/json_decode.md +++ b/docs/internals/builtins/json/json_decode.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/json_decode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/json_decode.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/json/json_encode.md b/docs/internals/builtins/json/json_encode.md index 76ea7c5c90..617cdfb465 100644 --- a/docs/internals/builtins/json/json_encode.md +++ b/docs/internals/builtins/json/json_encode.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/json_encode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/json_encode.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/json/json_last_error.md b/docs/internals/builtins/json/json_last_error.md index 75cad91029..332b25d613 100644 --- a/docs/internals/builtins/json/json_last_error.md +++ b/docs/internals/builtins/json/json_last_error.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/json_last_error.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/json_last_error.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/json/json_last_error_msg.md b/docs/internals/builtins/json/json_last_error_msg.md index 4011a5f7b3..0381ce5ca1 100644 --- a/docs/internals/builtins/json/json_last_error_msg.md +++ b/docs/internals/builtins/json/json_last_error_msg.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **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/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/json/json_validate.md b/docs/internals/builtins/json/json_validate.md index b44622342e..569ef7dd1a 100644 --- a/docs/internals/builtins/json/json_validate.md +++ b/docs/internals/builtins/json/json_validate.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/json_validate.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/json_validate.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/abs.md b/docs/internals/builtins/math/abs.md index bfb4302953..c75bc358b2 100644 --- a/docs/internals/builtins/math/abs.md +++ b/docs/internals/builtins/math/abs.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/abs.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/abs.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/acos.md b/docs/internals/builtins/math/acos.md index ddcf26a7de..2ff38f46d2 100644 --- a/docs/internals/builtins/math/acos.md +++ b/docs/internals/builtins/math/acos.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/acos.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/acos.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/asin.md b/docs/internals/builtins/math/asin.md index 1c6f974e70..e80e9c1f2b 100644 --- a/docs/internals/builtins/math/asin.md +++ b/docs/internals/builtins/math/asin.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/asin.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/asin.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/atan.md b/docs/internals/builtins/math/atan.md index ee89d401d9..2a06f53062 100644 --- a/docs/internals/builtins/math/atan.md +++ b/docs/internals/builtins/math/atan.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/atan.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/atan.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/atan2.md b/docs/internals/builtins/math/atan2.md index a55e711b41..0a67ba9d88 100644 --- a/docs/internals/builtins/math/atan2.md +++ b/docs/internals/builtins/math/atan2.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/atan2.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/atan2.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/ceil.md b/docs/internals/builtins/math/ceil.md index abbc303cc3..711f3c3c1a 100644 --- a/docs/internals/builtins/math/ceil.md +++ b/docs/internals/builtins/math/ceil.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/ceil.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/ceil.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/clamp.md b/docs/internals/builtins/math/clamp.md index 5672e25a74..6beee73410 100644 --- a/docs/internals/builtins/math/clamp.md +++ b/docs/internals/builtins/math/clamp.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/clamp.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/clamp.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/cos.md b/docs/internals/builtins/math/cos.md index f1e6396de1..705291aeaa 100644 --- a/docs/internals/builtins/math/cos.md +++ b/docs/internals/builtins/math/cos.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/cos.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/cos.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/cosh.md b/docs/internals/builtins/math/cosh.md index 1d6a9bed8c..19d5e1d277 100644 --- a/docs/internals/builtins/math/cosh.md +++ b/docs/internals/builtins/math/cosh.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/cosh.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/cosh.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/deg2rad.md b/docs/internals/builtins/math/deg2rad.md index 23e234f9c0..7142f76bd7 100644 --- a/docs/internals/builtins/math/deg2rad.md +++ b/docs/internals/builtins/math/deg2rad.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/deg2rad.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/deg2rad.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/exp.md b/docs/internals/builtins/math/exp.md index ffda6f112c..3ad6b6375a 100644 --- a/docs/internals/builtins/math/exp.md +++ b/docs/internals/builtins/math/exp.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/exp.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/exp.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/fdiv.md b/docs/internals/builtins/math/fdiv.md index 07f9117d48..11e0b3e2b9 100644 --- a/docs/internals/builtins/math/fdiv.md +++ b/docs/internals/builtins/math/fdiv.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/fdiv.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/fdiv.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/floor.md b/docs/internals/builtins/math/floor.md index 663ed6e4a5..61da401695 100644 --- a/docs/internals/builtins/math/floor.md +++ b/docs/internals/builtins/math/floor.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/floor.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/floor.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/fmod.md b/docs/internals/builtins/math/fmod.md index 3d1893c61e..311df4fbc4 100644 --- a/docs/internals/builtins/math/fmod.md +++ b/docs/internals/builtins/math/fmod.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/fmod.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/fmod.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/hypot.md b/docs/internals/builtins/math/hypot.md index 798a4b3aeb..ffd2137dbe 100644 --- a/docs/internals/builtins/math/hypot.md +++ b/docs/internals/builtins/math/hypot.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/hypot.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/hypot.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/intdiv.md b/docs/internals/builtins/math/intdiv.md index 8cbf5c2863..41965f27c1 100644 --- a/docs/internals/builtins/math/intdiv.md +++ b/docs/internals/builtins/math/intdiv.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/intdiv.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/intdiv.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/is_finite.md b/docs/internals/builtins/math/is_finite.md index a7526ef8ff..333cecb283 100644 --- a/docs/internals/builtins/math/is_finite.md +++ b/docs/internals/builtins/math/is_finite.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_finite.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_finite.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/is_infinite.md b/docs/internals/builtins/math/is_infinite.md index 84332f9841..99e28a95cd 100644 --- a/docs/internals/builtins/math/is_infinite.md +++ b/docs/internals/builtins/math/is_infinite.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_infinite.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_infinite.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/is_nan.md b/docs/internals/builtins/math/is_nan.md index ca36dd01ea..5df8804446 100644 --- a/docs/internals/builtins/math/is_nan.md +++ b/docs/internals/builtins/math/is_nan.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_nan.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_nan.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/log.md b/docs/internals/builtins/math/log.md index 92a61552a0..363c4a5b81 100644 --- a/docs/internals/builtins/math/log.md +++ b/docs/internals/builtins/math/log.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/log.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/log.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/log10.md b/docs/internals/builtins/math/log10.md index 1b1b9726ac..4153d6e202 100644 --- a/docs/internals/builtins/math/log10.md +++ b/docs/internals/builtins/math/log10.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/log10.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/log10.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/log2.md b/docs/internals/builtins/math/log2.md index 962714fc4e..2d6883c703 100644 --- a/docs/internals/builtins/math/log2.md +++ b/docs/internals/builtins/math/log2.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/log2.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/log2.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/max.md b/docs/internals/builtins/math/max.md index 32b277f179..c7887ecff2 100644 --- a/docs/internals/builtins/math/max.md +++ b/docs/internals/builtins/math/max.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/max.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/max.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/min.md b/docs/internals/builtins/math/min.md index cda5fc601c..b00d80e675 100644 --- a/docs/internals/builtins/math/min.md +++ b/docs/internals/builtins/math/min.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/min.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/min.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/mt_rand.md b/docs/internals/builtins/math/mt_rand.md index eb34eccaa3..8717f0dc8c 100644 --- a/docs/internals/builtins/math/mt_rand.md +++ b/docs/internals/builtins/math/mt_rand.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/mt_rand.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/mt_rand.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/pi.md b/docs/internals/builtins/math/pi.md index 395096715f..9c1762d540 100644 --- a/docs/internals/builtins/math/pi.md +++ b/docs/internals/builtins/math/pi.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/pi.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/pi.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/pow.md b/docs/internals/builtins/math/pow.md index 922a489909..a1aa99670a 100644 --- a/docs/internals/builtins/math/pow.md +++ b/docs/internals/builtins/math/pow.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/pow.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/pow.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/rad2deg.md b/docs/internals/builtins/math/rad2deg.md index b2d487e446..7284d2acfd 100644 --- a/docs/internals/builtins/math/rad2deg.md +++ b/docs/internals/builtins/math/rad2deg.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/rad2deg.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/rad2deg.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/rand.md b/docs/internals/builtins/math/rand.md index 52f1ead7a7..afe695aee5 100644 --- a/docs/internals/builtins/math/rand.md +++ b/docs/internals/builtins/math/rand.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/rand.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/rand.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/random_bytes.md b/docs/internals/builtins/math/random_bytes.md new file mode 100644 index 0000000000..243d00c39d --- /dev/null +++ b/docs/internals/builtins/math/random_bytes.md @@ -0,0 +1,56 @@ +--- +title: "random_bytes() — internals" +description: "Compiler internals for random_bytes(): lowering path, type checks, and runtime helpers." +sidebar: + order: 283 +--- + +## `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/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.random_bytes` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `checker_hook` +- **Result type source**: `checked` +- **Result ownership**: `may_alias_arguments` +- **Effects**: `static (16 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.random_bytes` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function random_bytes(int $length): string +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/random_bytes.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/random_bytes.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## 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 a622c3ad07..480893625d 100644 --- a/docs/internals/builtins/math/random_int.md +++ b/docs/internals/builtins/math/random_int.md @@ -2,7 +2,7 @@ title: "random_int() — internals" description: "Compiler internals for random_int(): lowering path, type checks, and runtime helpers." sidebar: - order: 283 + order: 284 --- ## `random_int()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/random_int.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/random_int.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/round.md b/docs/internals/builtins/math/round.md index ba93b95cdf..07228a53f3 100644 --- a/docs/internals/builtins/math/round.md +++ b/docs/internals/builtins/math/round.md @@ -2,7 +2,7 @@ title: "round() — internals" description: "Compiler internals for round(): lowering path, type checks, and runtime helpers." sidebar: - order: 284 + order: 285 --- ## `round()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/round.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/round.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/sin.md b/docs/internals/builtins/math/sin.md index a8d5a4068b..ab02a0bc27 100644 --- a/docs/internals/builtins/math/sin.md +++ b/docs/internals/builtins/math/sin.md @@ -2,7 +2,7 @@ title: "sin() — internals" description: "Compiler internals for sin(): lowering path, type checks, and runtime helpers." sidebar: - order: 285 + order: 286 --- ## `sin()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/sin.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/sin.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/sinh.md b/docs/internals/builtins/math/sinh.md index 61cdd2961f..9b755617c5 100644 --- a/docs/internals/builtins/math/sinh.md +++ b/docs/internals/builtins/math/sinh.md @@ -2,7 +2,7 @@ title: "sinh() — internals" description: "Compiler internals for sinh(): lowering path, type checks, and runtime helpers." sidebar: - order: 286 + order: 287 --- ## `sinh()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/sinh.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/sinh.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/sqrt.md b/docs/internals/builtins/math/sqrt.md index 9c81f407ea..037a00df32 100644 --- a/docs/internals/builtins/math/sqrt.md +++ b/docs/internals/builtins/math/sqrt.md @@ -2,7 +2,7 @@ title: "sqrt() — internals" description: "Compiler internals for sqrt(): lowering path, type checks, and runtime helpers." sidebar: - order: 287 + order: 288 --- ## `sqrt()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/sqrt.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/sqrt.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/tan.md b/docs/internals/builtins/math/tan.md index 48bc0891e3..ce4cbc3d14 100644 --- a/docs/internals/builtins/math/tan.md +++ b/docs/internals/builtins/math/tan.md @@ -2,7 +2,7 @@ title: "tan() — internals" description: "Compiler internals for tan(): lowering path, type checks, and runtime helpers." sidebar: - order: 288 + order: 289 --- ## `tan()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/tan.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/tan.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/tanh.md b/docs/internals/builtins/math/tanh.md index c4317b46b6..1d7f9238de 100644 --- a/docs/internals/builtins/math/tanh.md +++ b/docs/internals/builtins/math/tanh.md @@ -2,7 +2,7 @@ title: "tanh() — internals" description: "Compiler internals for tanh(): lowering path, type checks, and runtime helpers." sidebar: - order: 289 + order: 290 --- ## `tanh()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/tanh.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/tanh.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/misc/buffer_new.md b/docs/internals/builtins/misc/buffer_new.md index 1ea3184f78..4ca48ea5e7 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: 290 + order: 291 --- ## `buffer_new()` — internals diff --git a/docs/internals/builtins/misc/define.md b/docs/internals/builtins/misc/define.md index 89fcf45294..5fb0bca35a 100644 --- a/docs/internals/builtins/misc/define.md +++ b/docs/internals/builtins/misc/define.md @@ -2,7 +2,7 @@ title: "define() — internals" description: "Compiler internals for define(): lowering path, type checks, and runtime helpers." sidebar: - order: 291 + order: 292 --- ## `define()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/define.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/define.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/misc/defined.md b/docs/internals/builtins/misc/defined.md index 16a0c268c4..eba99b729d 100644 --- a/docs/internals/builtins/misc/defined.md +++ b/docs/internals/builtins/misc/defined.md @@ -2,7 +2,7 @@ title: "defined() — internals" description: "Compiler internals for defined(): lowering path, type checks, and runtime helpers." sidebar: - order: 292 + order: 293 --- ## `defined()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/defined.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/defined.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/misc/empty.md b/docs/internals/builtins/misc/empty.md index ed0f053063..a6d9cef843 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: 293 + order: 294 --- ## `empty()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1077](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1077) (`lower_empty`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1079](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1079) (`lower_empty`) - **Function symbol**: `lower_empty()` diff --git a/docs/internals/builtins/misc/header.md b/docs/internals/builtins/misc/header.md index 41ae675373..47de2c4e18 100644 --- a/docs/internals/builtins/misc/header.md +++ b/docs/internals/builtins/misc/header.md @@ -2,7 +2,7 @@ title: "header() — internals" description: "Compiler internals for header(): lowering path, type checks, and runtime helpers." sidebar: - order: 294 + order: 295 --- ## `header()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/header.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/header.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/misc/http_response_code.md b/docs/internals/builtins/misc/http_response_code.md index c669fa1d2b..b616a301ba 100644 --- a/docs/internals/builtins/misc/http_response_code.md +++ b/docs/internals/builtins/misc/http_response_code.md @@ -2,7 +2,7 @@ title: "http_response_code() — internals" description: "Compiler internals for http_response_code(): lowering path, type checks, and runtime helpers." sidebar: - order: 295 + order: 296 --- ## `http_response_code()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/http_response_code.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/http_response_code.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/misc/isset.md b/docs/internals/builtins/misc/isset.md index f408d69e42..2dac935c63 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: 296 + order: 297 --- ## `isset()` — internals diff --git a/docs/internals/builtins/misc/php_uname.md b/docs/internals/builtins/misc/php_uname.md index 9fb8ce7ac4..bb6835ac20 100644 --- a/docs/internals/builtins/misc/php_uname.md +++ b/docs/internals/builtins/misc/php_uname.md @@ -2,7 +2,7 @@ title: "php_uname() — internals" description: "Compiler internals for php_uname(): lowering path, type checks, and runtime helpers." sidebar: - order: 297 + order: 298 --- ## `php_uname()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/php_uname.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/php_uname.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/misc/phpversion.md b/docs/internals/builtins/misc/phpversion.md index b00c1dbb9e..dcde6c8f7b 100644 --- a/docs/internals/builtins/misc/phpversion.md +++ b/docs/internals/builtins/misc/phpversion.md @@ -2,7 +2,7 @@ title: "phpversion() — internals" description: "Compiler internals for phpversion(): lowering path, type checks, and runtime helpers." sidebar: - order: 298 + order: 299 --- ## `phpversion()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/phpversion.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/phpversion.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/misc/print_r.md b/docs/internals/builtins/misc/print_r.md index 397266b4af..eaf726e585 100644 --- a/docs/internals/builtins/misc/print_r.md +++ b/docs/internals/builtins/misc/print_r.md @@ -2,7 +2,7 @@ title: "print_r() — internals" description: "Compiler internals for print_r(): lowering path, type checks, and runtime helpers." sidebar: - order: 299 + order: 300 --- ## `print_r()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/print_r.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/print_r.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/misc/serialize.md b/docs/internals/builtins/misc/serialize.md index e560a2562c..548e16cf7a 100644 --- a/docs/internals/builtins/misc/serialize.md +++ b/docs/internals/builtins/misc/serialize.md @@ -2,7 +2,7 @@ title: "serialize() — internals" description: "Compiler internals for serialize(): lowering path, type checks, and runtime helpers." sidebar: - order: 300 + order: 301 --- ## `serialize()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/serialize.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/serialize.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/misc/unserialize.md b/docs/internals/builtins/misc/unserialize.md index b39d2d83ba..e9d26da8d4 100644 --- a/docs/internals/builtins/misc/unserialize.md +++ b/docs/internals/builtins/misc/unserialize.md @@ -2,7 +2,7 @@ title: "unserialize() — internals" description: "Compiler internals for unserialize(): lowering path, type checks, and runtime helpers." sidebar: - order: 301 + order: 302 --- ## `unserialize()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/unserialize.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/unserialize.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/misc/unset.md b/docs/internals/builtins/misc/unset.md index 3a532dac60..f55d242553 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: 302 + order: 303 --- ## `unset()` — internals diff --git a/docs/internals/builtins/misc/var_dump.md b/docs/internals/builtins/misc/var_dump.md index bd4ab0ed27..bc2a06f7ed 100644 --- a/docs/internals/builtins/misc/var_dump.md +++ b/docs/internals/builtins/misc/var_dump.md @@ -2,7 +2,7 @@ title: "var_dump() — internals" description: "Compiler internals for var_dump(): lowering path, type checks, and runtime helpers." sidebar: - order: 303 + order: 304 --- ## `var_dump()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/var_dump.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/var_dump.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr.md b/docs/internals/builtins/pointer/ptr.md index 5dcd86f303..eb5703ce1b 100644 --- a/docs/internals/builtins/pointer/ptr.md +++ b/docs/internals/builtins/pointer/ptr.md @@ -2,7 +2,7 @@ title: "ptr() — internals" description: "Compiler internals for ptr(): lowering path, type checks, and runtime helpers." sidebar: - order: 304 + order: 305 --- ## `ptr()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_get.md b/docs/internals/builtins/pointer/ptr_get.md index aab34da180..70416df943 100644 --- a/docs/internals/builtins/pointer/ptr_get.md +++ b/docs/internals/builtins/pointer/ptr_get.md @@ -2,7 +2,7 @@ title: "ptr_get() — internals" description: "Compiler internals for ptr_get(): lowering path, type checks, and runtime helpers." sidebar: - order: 305 + order: 306 --- ## `ptr_get()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_get.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_get.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_is_null.md b/docs/internals/builtins/pointer/ptr_is_null.md index ff2eb123f7..91b1098c5d 100644 --- a/docs/internals/builtins/pointer/ptr_is_null.md +++ b/docs/internals/builtins/pointer/ptr_is_null.md @@ -2,7 +2,7 @@ title: "ptr_is_null() — internals" description: "Compiler internals for ptr_is_null(): lowering path, type checks, and runtime helpers." sidebar: - order: 306 + order: 307 --- ## `ptr_is_null()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_is_null.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_is_null.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_null.md b/docs/internals/builtins/pointer/ptr_null.md index ced8a686d3..faffc9a4eb 100644 --- a/docs/internals/builtins/pointer/ptr_null.md +++ b/docs/internals/builtins/pointer/ptr_null.md @@ -2,7 +2,7 @@ title: "ptr_null() — internals" description: "Compiler internals for ptr_null(): lowering path, type checks, and runtime helpers." sidebar: - order: 307 + order: 308 --- ## `ptr_null()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_null.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_null.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_offset.md b/docs/internals/builtins/pointer/ptr_offset.md index aea80ee786..335f7eef6c 100644 --- a/docs/internals/builtins/pointer/ptr_offset.md +++ b/docs/internals/builtins/pointer/ptr_offset.md @@ -2,7 +2,7 @@ title: "ptr_offset() — internals" description: "Compiler internals for ptr_offset(): lowering path, type checks, and runtime helpers." sidebar: - order: 308 + order: 309 --- ## `ptr_offset()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_offset.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_offset.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_read16.md b/docs/internals/builtins/pointer/ptr_read16.md index c3e3849fb3..51bebf0456 100644 --- a/docs/internals/builtins/pointer/ptr_read16.md +++ b/docs/internals/builtins/pointer/ptr_read16.md @@ -2,7 +2,7 @@ title: "ptr_read16() — internals" description: "Compiler internals for ptr_read16(): lowering path, type checks, and runtime helpers." sidebar: - order: 309 + order: 310 --- ## `ptr_read16()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_read16.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_read16.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_read32.md b/docs/internals/builtins/pointer/ptr_read32.md index 3007149a1c..5d285cb044 100644 --- a/docs/internals/builtins/pointer/ptr_read32.md +++ b/docs/internals/builtins/pointer/ptr_read32.md @@ -2,7 +2,7 @@ title: "ptr_read32() — internals" description: "Compiler internals for ptr_read32(): lowering path, type checks, and runtime helpers." sidebar: - order: 310 + order: 311 --- ## `ptr_read32()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_read32.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_read32.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_read8.md b/docs/internals/builtins/pointer/ptr_read8.md index f2bb884741..678e592ab3 100644 --- a/docs/internals/builtins/pointer/ptr_read8.md +++ b/docs/internals/builtins/pointer/ptr_read8.md @@ -2,7 +2,7 @@ title: "ptr_read8() — internals" description: "Compiler internals for ptr_read8(): lowering path, type checks, and runtime helpers." sidebar: - order: 311 + order: 312 --- ## `ptr_read8()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_read8.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_read8.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_read_string.md b/docs/internals/builtins/pointer/ptr_read_string.md index 804bb7d4d0..5ad4d9ca07 100644 --- a/docs/internals/builtins/pointer/ptr_read_string.md +++ b/docs/internals/builtins/pointer/ptr_read_string.md @@ -2,7 +2,7 @@ title: "ptr_read_string() — internals" description: "Compiler internals for ptr_read_string(): lowering path, type checks, and runtime helpers." sidebar: - order: 312 + order: 313 --- ## `ptr_read_string()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_read_string.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_read_string.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_set.md b/docs/internals/builtins/pointer/ptr_set.md index dd3f8c582a..7602cbdfdb 100644 --- a/docs/internals/builtins/pointer/ptr_set.md +++ b/docs/internals/builtins/pointer/ptr_set.md @@ -2,7 +2,7 @@ title: "ptr_set() — internals" description: "Compiler internals for ptr_set(): lowering path, type checks, and runtime helpers." sidebar: - order: 313 + order: 314 --- ## `ptr_set()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_set.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_set.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_sizeof.md b/docs/internals/builtins/pointer/ptr_sizeof.md index ea818c9117..1396d267eb 100644 --- a/docs/internals/builtins/pointer/ptr_sizeof.md +++ b/docs/internals/builtins/pointer/ptr_sizeof.md @@ -2,7 +2,7 @@ title: "ptr_sizeof() — internals" description: "Compiler internals for ptr_sizeof(): lowering path, type checks, and runtime helpers." sidebar: - order: 314 + order: 315 --- ## `ptr_sizeof()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_sizeof.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_sizeof.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_write16.md b/docs/internals/builtins/pointer/ptr_write16.md index 1caf1971b0..62a2bd5503 100644 --- a/docs/internals/builtins/pointer/ptr_write16.md +++ b/docs/internals/builtins/pointer/ptr_write16.md @@ -2,7 +2,7 @@ title: "ptr_write16() — internals" description: "Compiler internals for ptr_write16(): lowering path, type checks, and runtime helpers." sidebar: - order: 315 + order: 316 --- ## `ptr_write16()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_write16.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_write16.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_write32.md b/docs/internals/builtins/pointer/ptr_write32.md index 77201c5009..3026dc4cd2 100644 --- a/docs/internals/builtins/pointer/ptr_write32.md +++ b/docs/internals/builtins/pointer/ptr_write32.md @@ -2,7 +2,7 @@ title: "ptr_write32() — internals" description: "Compiler internals for ptr_write32(): lowering path, type checks, and runtime helpers." sidebar: - order: 316 + order: 317 --- ## `ptr_write32()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_write32.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_write32.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_write8.md b/docs/internals/builtins/pointer/ptr_write8.md index a675533c7e..5391ebc3e9 100644 --- a/docs/internals/builtins/pointer/ptr_write8.md +++ b/docs/internals/builtins/pointer/ptr_write8.md @@ -2,7 +2,7 @@ title: "ptr_write8() — internals" description: "Compiler internals for ptr_write8(): lowering path, type checks, and runtime helpers." sidebar: - order: 317 + order: 318 --- ## `ptr_write8()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_write8.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_write8.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_write_string.md b/docs/internals/builtins/pointer/ptr_write_string.md index e867389092..3e50130717 100644 --- a/docs/internals/builtins/pointer/ptr_write_string.md +++ b/docs/internals/builtins/pointer/ptr_write_string.md @@ -2,7 +2,7 @@ title: "ptr_write_string() — internals" description: "Compiler internals for ptr_write_string(): lowering path, type checks, and runtime helpers." sidebar: - order: 318 + order: 319 --- ## `ptr_write_string()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_write_string.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_write_string.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/zval_free.md b/docs/internals/builtins/pointer/zval_free.md index 99001d3d2b..51488015dc 100644 --- a/docs/internals/builtins/pointer/zval_free.md +++ b/docs/internals/builtins/pointer/zval_free.md @@ -2,7 +2,7 @@ title: "zval_free() — internals" description: "Compiler internals for zval_free(): lowering path, type checks, and runtime helpers." sidebar: - order: 319 + order: 320 --- ## `zval_free()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/zval_free.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/zval_free.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/zval_pack.md b/docs/internals/builtins/pointer/zval_pack.md index 9c74a761bf..c8420a419b 100644 --- a/docs/internals/builtins/pointer/zval_pack.md +++ b/docs/internals/builtins/pointer/zval_pack.md @@ -2,7 +2,7 @@ title: "zval_pack() — internals" description: "Compiler internals for zval_pack(): lowering path, type checks, and runtime helpers." sidebar: - order: 320 + order: 321 --- ## `zval_pack()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/zval_pack.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/zval_pack.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/zval_type.md b/docs/internals/builtins/pointer/zval_type.md index 0460295be2..66d2de949c 100644 --- a/docs/internals/builtins/pointer/zval_type.md +++ b/docs/internals/builtins/pointer/zval_type.md @@ -2,7 +2,7 @@ title: "zval_type() — internals" description: "Compiler internals for zval_type(): lowering path, type checks, and runtime helpers." sidebar: - order: 321 + order: 322 --- ## `zval_type()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/zval_type.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/zval_type.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/zval_unpack.md b/docs/internals/builtins/pointer/zval_unpack.md index d6958134dc..812c4b8727 100644 --- a/docs/internals/builtins/pointer/zval_unpack.md +++ b/docs/internals/builtins/pointer/zval_unpack.md @@ -2,7 +2,7 @@ title: "zval_unpack() — internals" description: "Compiler internals for zval_unpack(): lowering path, type checks, and runtime helpers." sidebar: - order: 322 + order: 323 --- ## `zval_unpack()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/zval_unpack.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/zval_unpack.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/process/die.md b/docs/internals/builtins/process/die.md index 8bba9c65cd..8427fd6eb2 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: 323 + order: 324 --- ## `die()` — internals diff --git a/docs/internals/builtins/process/exec.md b/docs/internals/builtins/process/exec.md index fde8f43186..96d87f089a 100644 --- a/docs/internals/builtins/process/exec.md +++ b/docs/internals/builtins/process/exec.md @@ -2,7 +2,7 @@ title: "exec() — internals" description: "Compiler internals for exec(): lowering path, type checks, and runtime helpers." sidebar: - order: 324 + order: 325 --- ## `exec()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/exec.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/exec.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/process/exit.md b/docs/internals/builtins/process/exit.md index 84727f7e79..8fc14c0285 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: 325 + order: 326 --- ## `exit()` — internals diff --git a/docs/internals/builtins/process/passthru.md b/docs/internals/builtins/process/passthru.md index 6296d6f00a..5cec1b182f 100644 --- a/docs/internals/builtins/process/passthru.md +++ b/docs/internals/builtins/process/passthru.md @@ -2,7 +2,7 @@ title: "passthru() — internals" description: "Compiler internals for passthru(): lowering path, type checks, and runtime helpers." sidebar: - order: 326 + order: 327 --- ## `passthru()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/passthru.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/passthru.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/process/pclose.md b/docs/internals/builtins/process/pclose.md index 89217f3935..3ce406aaeb 100644 --- a/docs/internals/builtins/process/pclose.md +++ b/docs/internals/builtins/process/pclose.md @@ -2,7 +2,7 @@ title: "pclose() — internals" description: "Compiler internals for pclose(): lowering path, type checks, and runtime helpers." sidebar: - order: 327 + order: 328 --- ## `pclose()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/pclose.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/pclose.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/process/popen.md b/docs/internals/builtins/process/popen.md index cad65a9413..c0d120ac85 100644 --- a/docs/internals/builtins/process/popen.md +++ b/docs/internals/builtins/process/popen.md @@ -2,7 +2,7 @@ title: "popen() — internals" description: "Compiler internals for popen(): lowering path, type checks, and runtime helpers." sidebar: - order: 328 + order: 329 --- ## `popen()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/popen.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/popen.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/process/proc_close.md b/docs/internals/builtins/process/proc_close.md new file mode 100644 index 0000000000..7e3dbd38f3 --- /dev/null +++ b/docs/internals/builtins/process/proc_close.md @@ -0,0 +1,56 @@ +--- +title: "proc_close() — internals" +description: "Compiler internals for proc_close(): lowering path, type checks, and runtime helpers." +sidebar: + order: 330 +--- + +## `proc_close()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/io/proc_close.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/proc_close.rs) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.proc_close` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `signature` +- **Result type source**: `declared` +- **Result ownership**: `may_alias_arguments` +- **Effects**: `static (16 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.proc_close` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function proc_close(resource $process): int +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/proc_close.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/proc_close.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## Cross-references + +- [User reference for `proc_close()`](../../../php/builtins/process/proc_close.md) diff --git a/docs/internals/builtins/process/proc_get_status.md b/docs/internals/builtins/process/proc_get_status.md new file mode 100644 index 0000000000..97eacc1bc3 --- /dev/null +++ b/docs/internals/builtins/process/proc_get_status.md @@ -0,0 +1,56 @@ +--- +title: "proc_get_status() — internals" +description: "Compiler internals for proc_get_status(): lowering path, type checks, and runtime helpers." +sidebar: + order: 331 +--- + +## `proc_get_status()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/io/proc_get_status.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/proc_get_status.rs) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.proc_get_status` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `checker_hook` +- **Result type source**: `checked` +- **Result ownership**: `may_alias_arguments` +- **Effects**: `static (16 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.proc_get_status` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function proc_get_status(resource $process): array|false +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/proc_get_status.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/proc_get_status.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## Cross-references + +- [User reference for `proc_get_status()`](../../../php/builtins/process/proc_get_status.md) diff --git a/docs/internals/builtins/process/proc_open.md b/docs/internals/builtins/process/proc_open.md new file mode 100644 index 0000000000..d23795d67a --- /dev/null +++ b/docs/internals/builtins/process/proc_open.md @@ -0,0 +1,58 @@ +--- +title: "proc_open() — internals" +description: "Compiler internals for proc_open(): lowering path, type checks, and runtime helpers." +sidebar: + order: 332 +--- + +## `proc_open()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/io/proc_open.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/proc_open.rs) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.proc_open` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `checker_hook` +- **Result type source**: `checked` +- **Result ownership**: `may_alias_arguments` +- **Effects**: `static (16 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.proc_open` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function proc_open(array|string $command, array $descriptor_spec, array &$pipes, ?string $cwd = null, ?array $env_vars = null, ?array $options = null): resource|false +``` + +## What the type checker enforces + +- **Arity**: takes 3–6 arguments (3 optional). +- **By-reference parameters**: `$pipes`. + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/proc_open.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/proc_open.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$pipes`. + +## Cross-references + +- [User reference for `proc_open()`](../../../php/builtins/process/proc_open.md) diff --git a/docs/internals/builtins/process/proc_terminate.md b/docs/internals/builtins/process/proc_terminate.md new file mode 100644 index 0000000000..71defb5ff3 --- /dev/null +++ b/docs/internals/builtins/process/proc_terminate.md @@ -0,0 +1,56 @@ +--- +title: "proc_terminate() — internals" +description: "Compiler internals for proc_terminate(): lowering path, type checks, and runtime helpers." +sidebar: + order: 333 +--- + +## `proc_terminate()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/io/proc_terminate.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/proc_terminate.rs) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.proc_terminate` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `checker_hook` +- **Result type source**: `checked` +- **Result ownership**: `may_alias_arguments` +- **Effects**: `static (16 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.proc_terminate` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function proc_terminate(resource $process, int $signal = 15): bool +``` + +## What the type checker enforces + +- **Arity**: takes 1–2 arguments (1 optional). + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/proc_terminate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/proc_terminate.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## Cross-references + +- [User reference for `proc_terminate()`](../../../php/builtins/process/proc_terminate.md) diff --git a/docs/internals/builtins/process/readline.md b/docs/internals/builtins/process/readline.md index b41f1dbdbb..6c7abe1fd5 100644 --- a/docs/internals/builtins/process/readline.md +++ b/docs/internals/builtins/process/readline.md @@ -2,7 +2,7 @@ title: "readline() — internals" description: "Compiler internals for readline(): lowering path, type checks, and runtime helpers." sidebar: - order: 329 + order: 334 --- ## `readline()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/readline.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/readline.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/process/shell_exec.md b/docs/internals/builtins/process/shell_exec.md index 87f9c0d2b4..7358511de8 100644 --- a/docs/internals/builtins/process/shell_exec.md +++ b/docs/internals/builtins/process/shell_exec.md @@ -2,7 +2,7 @@ title: "shell_exec() — internals" description: "Compiler internals for shell_exec(): lowering path, type checks, and runtime helpers." sidebar: - order: 330 + order: 335 --- ## `shell_exec()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/shell_exec.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/shell_exec.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/process/sleep.md b/docs/internals/builtins/process/sleep.md index cf45fa569d..d947b58674 100644 --- a/docs/internals/builtins/process/sleep.md +++ b/docs/internals/builtins/process/sleep.md @@ -2,7 +2,7 @@ title: "sleep() — internals" description: "Compiler internals for sleep(): lowering path, type checks, and runtime helpers." sidebar: - order: 331 + order: 336 --- ## `sleep()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/sleep.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/sleep.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/process/system.md b/docs/internals/builtins/process/system.md index 923892eedd..bb02c5f580 100644 --- a/docs/internals/builtins/process/system.md +++ b/docs/internals/builtins/process/system.md @@ -2,7 +2,7 @@ title: "system() — internals" description: "Compiler internals for system(): lowering path, type checks, and runtime helpers." sidebar: - order: 332 + order: 337 --- ## `system()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/system.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/system.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/process/usleep.md b/docs/internals/builtins/process/usleep.md index 48063dff4d..41f2bdcf68 100644 --- a/docs/internals/builtins/process/usleep.md +++ b/docs/internals/builtins/process/usleep.md @@ -2,7 +2,7 @@ title: "usleep() — internals" description: "Compiler internals for usleep(): lowering path, type checks, and runtime helpers." sidebar: - order: 333 + order: 338 --- ## `usleep()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/usleep.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/usleep.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/regex/mb_ereg_match.md b/docs/internals/builtins/regex/mb_ereg_match.md index 0fb8587a03..44a4b51105 100644 --- a/docs/internals/builtins/regex/mb_ereg_match.md +++ b/docs/internals/builtins/regex/mb_ereg_match.md @@ -2,7 +2,7 @@ title: "mb_ereg_match() — internals" description: "Compiler internals for mb_ereg_match(): lowering path, type checks, and runtime helpers." sidebar: - order: 334 + order: 339 --- ## `mb_ereg_match()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/mb_ereg_match.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/mb_ereg_match.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/regex/preg_match.md b/docs/internals/builtins/regex/preg_match.md index 9f4621b58d..ff5b761619 100644 --- a/docs/internals/builtins/regex/preg_match.md +++ b/docs/internals/builtins/regex/preg_match.md @@ -2,7 +2,7 @@ title: "preg_match() — internals" description: "Compiler internals for preg_match(): lowering path, type checks, and runtime helpers." sidebar: - order: 335 + order: 340 --- ## `preg_match()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/preg_match.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/preg_match.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## 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 diff --git a/docs/internals/builtins/regex/preg_match_all.md b/docs/internals/builtins/regex/preg_match_all.md index 5fa6f209e6..e3d922dbf4 100644 --- a/docs/internals/builtins/regex/preg_match_all.md +++ b/docs/internals/builtins/regex/preg_match_all.md @@ -2,7 +2,7 @@ title: "preg_match_all() — internals" description: "Compiler internals for preg_match_all(): lowering path, type checks, and runtime helpers." sidebar: - order: 336 + order: 341 --- ## `preg_match_all()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/preg_match_all.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/preg_match_all.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/regex/preg_replace.md b/docs/internals/builtins/regex/preg_replace.md index e7a95abb51..2d02547e9e 100644 --- a/docs/internals/builtins/regex/preg_replace.md +++ b/docs/internals/builtins/regex/preg_replace.md @@ -2,7 +2,7 @@ title: "preg_replace() — internals" description: "Compiler internals for preg_replace(): lowering path, type checks, and runtime helpers." sidebar: - order: 337 + order: 342 --- ## `preg_replace()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/preg_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/preg_replace.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/regex/preg_replace_callback.md b/docs/internals/builtins/regex/preg_replace_callback.md index 1f10c09dca..55ca194781 100644 --- a/docs/internals/builtins/regex/preg_replace_callback.md +++ b/docs/internals/builtins/regex/preg_replace_callback.md @@ -2,7 +2,7 @@ title: "preg_replace_callback() — internals" description: "Compiler internals for preg_replace_callback(): lowering path, type checks, and runtime helpers." sidebar: - order: 338 + order: 343 --- ## `preg_replace_callback()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/preg_replace_callback.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/preg_replace_callback.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/regex/preg_split.md b/docs/internals/builtins/regex/preg_split.md index f5c52fd6e6..6a83e823fb 100644 --- a/docs/internals/builtins/regex/preg_split.md +++ b/docs/internals/builtins/regex/preg_split.md @@ -2,7 +2,7 @@ title: "preg_split() — internals" description: "Compiler internals for preg_split(): lowering path, type checks, and runtime helpers." sidebar: - order: 339 + order: 344 --- ## `preg_split()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/preg_split.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/preg_split.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/spl/iterator_apply.md b/docs/internals/builtins/spl/iterator_apply.md index e44a38dd7c..834b4947d4 100644 --- a/docs/internals/builtins/spl/iterator_apply.md +++ b/docs/internals/builtins/spl/iterator_apply.md @@ -2,7 +2,7 @@ title: "iterator_apply() — internals" description: "Compiler internals for iterator_apply(): lowering path, type checks, and runtime helpers." sidebar: - order: 340 + order: 345 --- ## `iterator_apply()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/spl/iterator_apply.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/iterator_apply.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/spl/iterator_count.md b/docs/internals/builtins/spl/iterator_count.md index 899bf3221d..c5bf30290b 100644 --- a/docs/internals/builtins/spl/iterator_count.md +++ b/docs/internals/builtins/spl/iterator_count.md @@ -2,7 +2,7 @@ title: "iterator_count() — internals" description: "Compiler internals for iterator_count(): lowering path, type checks, and runtime helpers." sidebar: - order: 341 + order: 346 --- ## `iterator_count()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/spl/iterator_count.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/iterator_count.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/spl/iterator_to_array.md b/docs/internals/builtins/spl/iterator_to_array.md index 66e974129d..c71df99263 100644 --- a/docs/internals/builtins/spl/iterator_to_array.md +++ b/docs/internals/builtins/spl/iterator_to_array.md @@ -2,7 +2,7 @@ title: "iterator_to_array() — internals" description: "Compiler internals for iterator_to_array(): lowering path, type checks, and runtime helpers." sidebar: - order: 342 + order: 347 --- ## `iterator_to_array()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/spl/iterator_to_array.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/iterator_to_array.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/spl/spl_autoload.md b/docs/internals/builtins/spl/spl_autoload.md index 7a4761996d..9fb5b08e20 100644 --- a/docs/internals/builtins/spl/spl_autoload.md +++ b/docs/internals/builtins/spl/spl_autoload.md @@ -2,7 +2,7 @@ title: "spl_autoload() — internals" description: "Compiler internals for spl_autoload(): lowering path, type checks, and runtime helpers." sidebar: - order: 343 + order: 348 --- ## `spl_autoload()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/spl/spl_autoload.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/spl_autoload.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/spl/spl_autoload_call.md b/docs/internals/builtins/spl/spl_autoload_call.md index a81064d8ba..66b4811ed0 100644 --- a/docs/internals/builtins/spl/spl_autoload_call.md +++ b/docs/internals/builtins/spl/spl_autoload_call.md @@ -2,7 +2,7 @@ title: "spl_autoload_call() — internals" description: "Compiler internals for spl_autoload_call(): lowering path, type checks, and runtime helpers." sidebar: - order: 344 + order: 349 --- ## `spl_autoload_call()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/spl/spl_autoload_call.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/spl_autoload_call.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/spl/spl_autoload_extensions.md b/docs/internals/builtins/spl/spl_autoload_extensions.md index a728036c75..bf9ff7e1c0 100644 --- a/docs/internals/builtins/spl/spl_autoload_extensions.md +++ b/docs/internals/builtins/spl/spl_autoload_extensions.md @@ -2,7 +2,7 @@ title: "spl_autoload_extensions() — internals" description: "Compiler internals for spl_autoload_extensions(): lowering path, type checks, and runtime helpers." sidebar: - order: 345 + order: 350 --- ## `spl_autoload_extensions()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/spl/spl_autoload_extensions.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/spl_autoload_extensions.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/spl/spl_autoload_functions.md b/docs/internals/builtins/spl/spl_autoload_functions.md index d53c3de29c..469ff5ea43 100644 --- a/docs/internals/builtins/spl/spl_autoload_functions.md +++ b/docs/internals/builtins/spl/spl_autoload_functions.md @@ -2,7 +2,7 @@ title: "spl_autoload_functions() — internals" description: "Compiler internals for spl_autoload_functions(): lowering path, type checks, and runtime helpers." sidebar: - order: 346 + order: 351 --- ## `spl_autoload_functions()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/spl/spl_autoload_functions.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/spl_autoload_functions.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/spl/spl_autoload_register.md b/docs/internals/builtins/spl/spl_autoload_register.md index c96f3faa3c..47cfecbd00 100644 --- a/docs/internals/builtins/spl/spl_autoload_register.md +++ b/docs/internals/builtins/spl/spl_autoload_register.md @@ -2,7 +2,7 @@ title: "spl_autoload_register() — internals" description: "Compiler internals for spl_autoload_register(): lowering path, type checks, and runtime helpers." sidebar: - order: 347 + order: 352 --- ## `spl_autoload_register()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/spl/spl_autoload_register.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/spl_autoload_register.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/spl/spl_autoload_unregister.md b/docs/internals/builtins/spl/spl_autoload_unregister.md index dd6269c1b5..348d4daff6 100644 --- a/docs/internals/builtins/spl/spl_autoload_unregister.md +++ b/docs/internals/builtins/spl/spl_autoload_unregister.md @@ -2,7 +2,7 @@ title: "spl_autoload_unregister() — internals" description: "Compiler internals for spl_autoload_unregister(): lowering path, type checks, and runtime helpers." sidebar: - order: 348 + order: 353 --- ## `spl_autoload_unregister()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/spl/spl_autoload_unregister.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/spl_autoload_unregister.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/spl/spl_classes.md b/docs/internals/builtins/spl/spl_classes.md index 74fd3bbbce..61cb87ab9a 100644 --- a/docs/internals/builtins/spl/spl_classes.md +++ b/docs/internals/builtins/spl/spl_classes.md @@ -2,7 +2,7 @@ title: "spl_classes() — internals" description: "Compiler internals for spl_classes(): lowering path, type checks, and runtime helpers." sidebar: - order: 349 + order: 354 --- ## `spl_classes()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/spl/spl_classes.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/spl_classes.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/spl/spl_object_hash.md b/docs/internals/builtins/spl/spl_object_hash.md index 3bf740ec9e..3fca2aa8fa 100644 --- a/docs/internals/builtins/spl/spl_object_hash.md +++ b/docs/internals/builtins/spl/spl_object_hash.md @@ -2,7 +2,7 @@ title: "spl_object_hash() — internals" description: "Compiler internals for spl_object_hash(): lowering path, type checks, and runtime helpers." sidebar: - order: 350 + order: 355 --- ## `spl_object_hash()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/spl/spl_object_hash.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/spl_object_hash.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/spl/spl_object_id.md b/docs/internals/builtins/spl/spl_object_id.md index d1c850700e..fe1fd25630 100644 --- a/docs/internals/builtins/spl/spl_object_id.md +++ b/docs/internals/builtins/spl/spl_object_id.md @@ -2,7 +2,7 @@ title: "spl_object_id() — internals" description: "Compiler internals for spl_object_id(): lowering path, type checks, and runtime helpers." sidebar: - order: 351 + order: 356 --- ## `spl_object_id()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/spl/spl_object_id.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/spl_object_id.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/streams/fsockopen.md b/docs/internals/builtins/streams/fsockopen.md index 5cb8571649..f32e34826e 100644 --- a/docs/internals/builtins/streams/fsockopen.md +++ b/docs/internals/builtins/streams/fsockopen.md @@ -2,7 +2,7 @@ title: "fsockopen() — internals" description: "Compiler internals for fsockopen(): lowering path, type checks, and runtime helpers." sidebar: - order: 352 + order: 357 --- ## `fsockopen()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fsockopen.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fsockopen.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -27,7 +27,7 @@ sidebar: - **Result type source**: `checked` - **Result ownership**: `may_alias_arguments` - **Effects**: `static (16 declared effects)` -- **Requirements**: `static (0 requirements)` +- **Requirements**: `shared` - **Callable policy**: `static_only` - **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function fsockopen(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed +function fsockopen(string $hostname, int $port, int &$error_code = null, string &$error_message = null, float $timeout = null): mixed ``` ## What the type checker enforces diff --git a/docs/internals/builtins/streams/pfsockopen.md b/docs/internals/builtins/streams/pfsockopen.md index a4f7a77a8c..508caf4b7a 100644 --- a/docs/internals/builtins/streams/pfsockopen.md +++ b/docs/internals/builtins/streams/pfsockopen.md @@ -2,7 +2,7 @@ title: "pfsockopen() — internals" description: "Compiler internals for pfsockopen(): lowering path, type checks, and runtime helpers." sidebar: - order: 353 + order: 358 --- ## `pfsockopen()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/pfsockopen.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/pfsockopen.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function pfsockopen(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed +function pfsockopen(string $hostname, int $port, int &$error_code = null, string &$error_message = null, float $timeout = null): mixed ``` ## What the type checker enforces diff --git a/docs/internals/builtins/streams/stream_bucket_append.md b/docs/internals/builtins/streams/stream_bucket_append.md index cd51dca630..b07e599d7b 100644 --- a/docs/internals/builtins/streams/stream_bucket_append.md +++ b/docs/internals/builtins/streams/stream_bucket_append.md @@ -2,7 +2,7 @@ title: "stream_bucket_append() — internals" description: "Compiler internals for stream_bucket_append(): lowering path, type checks, and runtime helpers." sidebar: - order: 354 + order: 359 --- ## `stream_bucket_append()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_bucket_append.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_bucket_append.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/streams/stream_bucket_prepend.md b/docs/internals/builtins/streams/stream_bucket_prepend.md index 5312339c43..718a691309 100644 --- a/docs/internals/builtins/streams/stream_bucket_prepend.md +++ b/docs/internals/builtins/streams/stream_bucket_prepend.md @@ -2,7 +2,7 @@ title: "stream_bucket_prepend() — internals" description: "Compiler internals for stream_bucket_prepend(): lowering path, type checks, and runtime helpers." sidebar: - order: 355 + order: 360 --- ## `stream_bucket_prepend()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_bucket_prepend.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_bucket_prepend.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/streams/stream_filter_append.md b/docs/internals/builtins/streams/stream_filter_append.md index 5c2679a385..1799e9398e 100644 --- a/docs/internals/builtins/streams/stream_filter_append.md +++ b/docs/internals/builtins/streams/stream_filter_append.md @@ -2,7 +2,7 @@ title: "stream_filter_append() — internals" description: "Compiler internals for stream_filter_append(): lowering path, type checks, and runtime helpers." sidebar: - order: 356 + order: 361 --- ## `stream_filter_append()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_filter_append.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_filter_append.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/streams/stream_filter_prepend.md b/docs/internals/builtins/streams/stream_filter_prepend.md index f150244362..06e49b01e1 100644 --- a/docs/internals/builtins/streams/stream_filter_prepend.md +++ b/docs/internals/builtins/streams/stream_filter_prepend.md @@ -2,7 +2,7 @@ title: "stream_filter_prepend() — internals" description: "Compiler internals for stream_filter_prepend(): lowering path, type checks, and runtime helpers." sidebar: - order: 357 + order: 362 --- ## `stream_filter_prepend()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_filter_prepend.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_filter_prepend.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/addslashes.md b/docs/internals/builtins/string/addslashes.md index cddc970ec0..f5990046e0 100644 --- a/docs/internals/builtins/string/addslashes.md +++ b/docs/internals/builtins/string/addslashes.md @@ -2,7 +2,7 @@ title: "addslashes() — internals" description: "Compiler internals for addslashes(): lowering path, type checks, and runtime helpers." sidebar: - order: 358 + order: 363 --- ## `addslashes()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/addslashes.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/addslashes.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/base64_decode.md b/docs/internals/builtins/string/base64_decode.md index 1541ba80d9..7af1bc196e 100644 --- a/docs/internals/builtins/string/base64_decode.md +++ b/docs/internals/builtins/string/base64_decode.md @@ -2,7 +2,7 @@ title: "base64_decode() — internals" description: "Compiler internals for base64_decode(): lowering path, type checks, and runtime helpers." sidebar: - order: 359 + order: 364 --- ## `base64_decode()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/base64_decode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/base64_decode.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/base64_encode.md b/docs/internals/builtins/string/base64_encode.md index c3d309a869..571078cd57 100644 --- a/docs/internals/builtins/string/base64_encode.md +++ b/docs/internals/builtins/string/base64_encode.md @@ -2,7 +2,7 @@ title: "base64_encode() — internals" description: "Compiler internals for base64_encode(): lowering path, type checks, and runtime helpers." sidebar: - order: 360 + order: 365 --- ## `base64_encode()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/base64_encode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/base64_encode.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/bin2hex.md b/docs/internals/builtins/string/bin2hex.md index e0644aa595..3fa53c40f5 100644 --- a/docs/internals/builtins/string/bin2hex.md +++ b/docs/internals/builtins/string/bin2hex.md @@ -2,7 +2,7 @@ title: "bin2hex() — internals" description: "Compiler internals for bin2hex(): lowering path, type checks, and runtime helpers." sidebar: - order: 361 + order: 366 --- ## `bin2hex()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/bin2hex.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/bin2hex.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/chop.md b/docs/internals/builtins/string/chop.md index 104a37cabd..b1dd720da8 100644 --- a/docs/internals/builtins/string/chop.md +++ b/docs/internals/builtins/string/chop.md @@ -2,7 +2,7 @@ title: "chop() — internals" description: "Compiler internals for chop(): lowering path, type checks, and runtime helpers." sidebar: - order: 362 + order: 367 --- ## `chop()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/chop.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/chop.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/chr.md b/docs/internals/builtins/string/chr.md index 772d15ad72..6bb9e02d5a 100644 --- a/docs/internals/builtins/string/chr.md +++ b/docs/internals/builtins/string/chr.md @@ -2,7 +2,7 @@ title: "chr() — internals" description: "Compiler internals for chr(): lowering path, type checks, and runtime helpers." sidebar: - order: 363 + order: 368 --- ## `chr()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/chr.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/chr.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/crc32.md b/docs/internals/builtins/string/crc32.md index fd48b16b6c..596c426af0 100644 --- a/docs/internals/builtins/string/crc32.md +++ b/docs/internals/builtins/string/crc32.md @@ -2,7 +2,7 @@ title: "crc32() — internals" description: "Compiler internals for crc32(): lowering path, type checks, and runtime helpers." sidebar: - order: 364 + order: 369 --- ## `crc32()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/crc32.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/crc32.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/escapeshellarg.md b/docs/internals/builtins/string/escapeshellarg.md new file mode 100644 index 0000000000..87b4bdcaf3 --- /dev/null +++ b/docs/internals/builtins/string/escapeshellarg.md @@ -0,0 +1,56 @@ +--- +title: "escapeshellarg() — internals" +description: "Compiler internals for escapeshellarg(): lowering path, type checks, and runtime helpers." +sidebar: + order: 370 +--- + +## `escapeshellarg()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/string/escapeshellarg.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/escapeshellarg.rs) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.string.escape_shell_arg` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `checker_hook` +- **Result type source**: `checked` +- **Result ownership**: `fresh` +- **Effects**: `static (1 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `dynamic` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.string.escape_shell_arg` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function escapeshellarg(string $arg): string +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/escapeshellarg.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/escapeshellarg.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## Cross-references + +- [User reference for `escapeshellarg()`](../../../php/builtins/string/escapeshellarg.md) diff --git a/docs/internals/builtins/string/escapeshellcmd.md b/docs/internals/builtins/string/escapeshellcmd.md new file mode 100644 index 0000000000..33e5fec848 --- /dev/null +++ b/docs/internals/builtins/string/escapeshellcmd.md @@ -0,0 +1,56 @@ +--- +title: "escapeshellcmd() — internals" +description: "Compiler internals for escapeshellcmd(): lowering path, type checks, and runtime helpers." +sidebar: + order: 371 +--- + +## `escapeshellcmd()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/string/escapeshellcmd.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/escapeshellcmd.rs) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.string.escape_shell_cmd` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `checker_hook` +- **Result type source**: `checked` +- **Result ownership**: `fresh` +- **Effects**: `static (1 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `dynamic` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.string.escape_shell_cmd` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function escapeshellcmd(string $command): string +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/escapeshellcmd.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/escapeshellcmd.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## Cross-references + +- [User reference for `escapeshellcmd()`](../../../php/builtins/string/escapeshellcmd.md) diff --git a/docs/internals/builtins/string/explode.md b/docs/internals/builtins/string/explode.md index ff191813cd..cf8dd023d9 100644 --- a/docs/internals/builtins/string/explode.md +++ b/docs/internals/builtins/string/explode.md @@ -2,7 +2,7 @@ title: "explode() — internals" description: "Compiler internals for explode(): lowering path, type checks, and runtime helpers." sidebar: - order: 365 + order: 372 --- ## `explode()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/explode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/explode.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/grapheme_strrev.md b/docs/internals/builtins/string/grapheme_strrev.md index 118d7c541d..67b3f69cff 100644 --- a/docs/internals/builtins/string/grapheme_strrev.md +++ b/docs/internals/builtins/string/grapheme_strrev.md @@ -2,7 +2,7 @@ title: "grapheme_strrev() — internals" description: "Compiler internals for grapheme_strrev(): lowering path, type checks, and runtime helpers." sidebar: - order: 366 + order: 373 --- ## `grapheme_strrev()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/grapheme_strrev.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/grapheme_strrev.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/gzcompress.md b/docs/internals/builtins/string/gzcompress.md index 38c5214365..7c36964f93 100644 --- a/docs/internals/builtins/string/gzcompress.md +++ b/docs/internals/builtins/string/gzcompress.md @@ -2,7 +2,7 @@ title: "gzcompress() — internals" description: "Compiler internals for gzcompress(): lowering path, type checks, and runtime helpers." sidebar: - order: 367 + order: 374 --- ## `gzcompress()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/gzcompress.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/gzcompress.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/gzdeflate.md b/docs/internals/builtins/string/gzdeflate.md index 8850b5bd82..02c755de3e 100644 --- a/docs/internals/builtins/string/gzdeflate.md +++ b/docs/internals/builtins/string/gzdeflate.md @@ -2,7 +2,7 @@ title: "gzdeflate() — internals" description: "Compiler internals for gzdeflate(): lowering path, type checks, and runtime helpers." sidebar: - order: 368 + order: 375 --- ## `gzdeflate()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/gzdeflate.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/gzdeflate.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/gzinflate.md b/docs/internals/builtins/string/gzinflate.md index 688f38ae08..2427952a2d 100644 --- a/docs/internals/builtins/string/gzinflate.md +++ b/docs/internals/builtins/string/gzinflate.md @@ -2,7 +2,7 @@ title: "gzinflate() — internals" description: "Compiler internals for gzinflate(): lowering path, type checks, and runtime helpers." sidebar: - order: 369 + order: 376 --- ## `gzinflate()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/gzinflate.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/gzinflate.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/gzuncompress.md b/docs/internals/builtins/string/gzuncompress.md index 0e0941bec3..22cdedff12 100644 --- a/docs/internals/builtins/string/gzuncompress.md +++ b/docs/internals/builtins/string/gzuncompress.md @@ -2,7 +2,7 @@ title: "gzuncompress() — internals" description: "Compiler internals for gzuncompress(): lowering path, type checks, and runtime helpers." sidebar: - order: 370 + order: 377 --- ## `gzuncompress()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/gzuncompress.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/gzuncompress.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/hash.md b/docs/internals/builtins/string/hash.md index 27b5cad113..157892aed3 100644 --- a/docs/internals/builtins/string/hash.md +++ b/docs/internals/builtins/string/hash.md @@ -2,7 +2,7 @@ title: "hash() — internals" description: "Compiler internals for hash(): lowering path, type checks, and runtime helpers." sidebar: - order: 371 + order: 378 --- ## `hash()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/hash.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/hash.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/hash_algos.md b/docs/internals/builtins/string/hash_algos.md index ce3eb970ec..3ce645d310 100644 --- a/docs/internals/builtins/string/hash_algos.md +++ b/docs/internals/builtins/string/hash_algos.md @@ -2,7 +2,7 @@ title: "hash_algos() — internals" description: "Compiler internals for hash_algos(): lowering path, type checks, and runtime helpers." sidebar: - order: 372 + order: 379 --- ## `hash_algos()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/hash_algos.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/hash_algos.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/hash_copy.md b/docs/internals/builtins/string/hash_copy.md index 577f035b28..c9e038c787 100644 --- a/docs/internals/builtins/string/hash_copy.md +++ b/docs/internals/builtins/string/hash_copy.md @@ -2,7 +2,7 @@ title: "hash_copy() — internals" description: "Compiler internals for hash_copy(): lowering path, type checks, and runtime helpers." sidebar: - order: 373 + order: 380 --- ## `hash_copy()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/hash_copy.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/hash_copy.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/hash_equals.md b/docs/internals/builtins/string/hash_equals.md index 9861643980..a095e560d3 100644 --- a/docs/internals/builtins/string/hash_equals.md +++ b/docs/internals/builtins/string/hash_equals.md @@ -2,7 +2,7 @@ title: "hash_equals() — internals" description: "Compiler internals for hash_equals(): lowering path, type checks, and runtime helpers." sidebar: - order: 374 + order: 381 --- ## `hash_equals()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/hash_equals.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/hash_equals.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/hash_final.md b/docs/internals/builtins/string/hash_final.md index 71f0a81105..507f0639bd 100644 --- a/docs/internals/builtins/string/hash_final.md +++ b/docs/internals/builtins/string/hash_final.md @@ -2,7 +2,7 @@ title: "hash_final() — internals" description: "Compiler internals for hash_final(): lowering path, type checks, and runtime helpers." sidebar: - order: 375 + order: 382 --- ## `hash_final()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/hash_final.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/hash_final.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/hash_hmac.md b/docs/internals/builtins/string/hash_hmac.md index 7d0fcfebac..3937544f27 100644 --- a/docs/internals/builtins/string/hash_hmac.md +++ b/docs/internals/builtins/string/hash_hmac.md @@ -2,7 +2,7 @@ title: "hash_hmac() — internals" description: "Compiler internals for hash_hmac(): lowering path, type checks, and runtime helpers." sidebar: - order: 376 + order: 383 --- ## `hash_hmac()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/hash_hmac.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/hash_hmac.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/hash_init.md b/docs/internals/builtins/string/hash_init.md index 1faf5ed5cb..566e5f3a1a 100644 --- a/docs/internals/builtins/string/hash_init.md +++ b/docs/internals/builtins/string/hash_init.md @@ -2,7 +2,7 @@ title: "hash_init() — internals" description: "Compiler internals for hash_init(): lowering path, type checks, and runtime helpers." sidebar: - order: 377 + order: 384 --- ## `hash_init()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/hash_init.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/hash_init.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/hash_update.md b/docs/internals/builtins/string/hash_update.md index ebf812188a..898d882b39 100644 --- a/docs/internals/builtins/string/hash_update.md +++ b/docs/internals/builtins/string/hash_update.md @@ -2,7 +2,7 @@ title: "hash_update() — internals" description: "Compiler internals for hash_update(): lowering path, type checks, and runtime helpers." sidebar: - order: 378 + order: 385 --- ## `hash_update()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/hash_update.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/hash_update.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/hex2bin.md b/docs/internals/builtins/string/hex2bin.md index cf67707389..77b0f526f5 100644 --- a/docs/internals/builtins/string/hex2bin.md +++ b/docs/internals/builtins/string/hex2bin.md @@ -2,7 +2,7 @@ title: "hex2bin() — internals" description: "Compiler internals for hex2bin(): lowering path, type checks, and runtime helpers." sidebar: - order: 379 + order: 386 --- ## `hex2bin()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/hex2bin.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/hex2bin.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/html_entity_decode.md b/docs/internals/builtins/string/html_entity_decode.md index bac1abacb2..9362050946 100644 --- a/docs/internals/builtins/string/html_entity_decode.md +++ b/docs/internals/builtins/string/html_entity_decode.md @@ -2,7 +2,7 @@ title: "html_entity_decode() — internals" description: "Compiler internals for html_entity_decode(): lowering path, type checks, and runtime helpers." sidebar: - order: 380 + order: 387 --- ## `html_entity_decode()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/html_entity_decode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/html_entity_decode.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/htmlentities.md b/docs/internals/builtins/string/htmlentities.md index 8830c0273c..f83d995469 100644 --- a/docs/internals/builtins/string/htmlentities.md +++ b/docs/internals/builtins/string/htmlentities.md @@ -2,7 +2,7 @@ title: "htmlentities() — internals" description: "Compiler internals for htmlentities(): lowering path, type checks, and runtime helpers." sidebar: - order: 381 + order: 388 --- ## `htmlentities()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/htmlentities.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/htmlentities.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/htmlspecialchars.md b/docs/internals/builtins/string/htmlspecialchars.md index f5c605b761..5a18558df7 100644 --- a/docs/internals/builtins/string/htmlspecialchars.md +++ b/docs/internals/builtins/string/htmlspecialchars.md @@ -2,7 +2,7 @@ title: "htmlspecialchars() — internals" description: "Compiler internals for htmlspecialchars(): lowering path, type checks, and runtime helpers." sidebar: - order: 382 + order: 389 --- ## `htmlspecialchars()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/htmlspecialchars.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/htmlspecialchars.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/implode.md b/docs/internals/builtins/string/implode.md index 467a07c0fa..5386580589 100644 --- a/docs/internals/builtins/string/implode.md +++ b/docs/internals/builtins/string/implode.md @@ -2,7 +2,7 @@ title: "implode() — internals" description: "Compiler internals for implode(): lowering path, type checks, and runtime helpers." sidebar: - order: 383 + order: 390 --- ## `implode()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/implode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/implode.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/inet_ntop.md b/docs/internals/builtins/string/inet_ntop.md index 6a886f6353..67eb0bf5f4 100644 --- a/docs/internals/builtins/string/inet_ntop.md +++ b/docs/internals/builtins/string/inet_ntop.md @@ -2,7 +2,7 @@ title: "inet_ntop() — internals" description: "Compiler internals for inet_ntop(): lowering path, type checks, and runtime helpers." sidebar: - order: 384 + order: 391 --- ## `inet_ntop()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/inet_ntop.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/inet_ntop.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/inet_pton.md b/docs/internals/builtins/string/inet_pton.md index 347d3e3411..a19e49c32a 100644 --- a/docs/internals/builtins/string/inet_pton.md +++ b/docs/internals/builtins/string/inet_pton.md @@ -2,7 +2,7 @@ title: "inet_pton() — internals" description: "Compiler internals for inet_pton(): lowering path, type checks, and runtime helpers." sidebar: - order: 385 + order: 392 --- ## `inet_pton()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/inet_pton.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/inet_pton.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/ip2long.md b/docs/internals/builtins/string/ip2long.md index 37da46d7ec..05381b1593 100644 --- a/docs/internals/builtins/string/ip2long.md +++ b/docs/internals/builtins/string/ip2long.md @@ -2,7 +2,7 @@ title: "ip2long() — internals" description: "Compiler internals for ip2long(): lowering path, type checks, and runtime helpers." sidebar: - order: 386 + order: 393 --- ## `ip2long()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/ip2long.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ip2long.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/lcfirst.md b/docs/internals/builtins/string/lcfirst.md index 9fadaf3249..40e073c94f 100644 --- a/docs/internals/builtins/string/lcfirst.md +++ b/docs/internals/builtins/string/lcfirst.md @@ -2,7 +2,7 @@ title: "lcfirst() — internals" description: "Compiler internals for lcfirst(): lowering path, type checks, and runtime helpers." sidebar: - order: 387 + order: 394 --- ## `lcfirst()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/lcfirst.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/lcfirst.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/long2ip.md b/docs/internals/builtins/string/long2ip.md index 5ceb7f6dc6..bcd842717d 100644 --- a/docs/internals/builtins/string/long2ip.md +++ b/docs/internals/builtins/string/long2ip.md @@ -2,7 +2,7 @@ title: "long2ip() — internals" description: "Compiler internals for long2ip(): lowering path, type checks, and runtime helpers." sidebar: - order: 388 + order: 395 --- ## `long2ip()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/long2ip.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/long2ip.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/ltrim.md b/docs/internals/builtins/string/ltrim.md index 0fde52fb26..ab69d701ad 100644 --- a/docs/internals/builtins/string/ltrim.md +++ b/docs/internals/builtins/string/ltrim.md @@ -2,7 +2,7 @@ title: "ltrim() — internals" description: "Compiler internals for ltrim(): lowering path, type checks, and runtime helpers." sidebar: - order: 389 + order: 396 --- ## `ltrim()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/ltrim.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ltrim.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/mb_strlen.md b/docs/internals/builtins/string/mb_strlen.md index ba2424aefa..4729ec4fde 100644 --- a/docs/internals/builtins/string/mb_strlen.md +++ b/docs/internals/builtins/string/mb_strlen.md @@ -2,7 +2,7 @@ title: "mb_strlen() — internals" description: "Compiler internals for mb_strlen(): lowering path, type checks, and runtime helpers." sidebar: - order: 390 + order: 397 --- ## `mb_strlen()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/mb_strlen.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/mb_strlen.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -27,7 +27,7 @@ sidebar: - **Result type source**: `checked` - **Result ownership**: `may_alias_arguments` - **Effects**: `static (16 declared effects)` -- **Requirements**: `static (1 requirements)` +- **Requirements**: `static (2 requirements)` - **Callable policy**: `static_only` - **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function mb_strlen(string $string, string $encoding = null): int +function mb_strlen(string $string, mixed $encoding = null): int ``` ## What the type checker enforces diff --git a/docs/internals/builtins/string/md5.md b/docs/internals/builtins/string/md5.md index 726d546996..a427214989 100644 --- a/docs/internals/builtins/string/md5.md +++ b/docs/internals/builtins/string/md5.md @@ -2,7 +2,7 @@ title: "md5() — internals" description: "Compiler internals for md5(): lowering path, type checks, and runtime helpers." sidebar: - order: 391 + order: 398 --- ## `md5()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/md5.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/md5.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/nl2br.md b/docs/internals/builtins/string/nl2br.md index 8acc079e8b..2afe808240 100644 --- a/docs/internals/builtins/string/nl2br.md +++ b/docs/internals/builtins/string/nl2br.md @@ -2,7 +2,7 @@ title: "nl2br() — internals" description: "Compiler internals for nl2br(): lowering path, type checks, and runtime helpers." sidebar: - order: 392 + order: 399 --- ## `nl2br()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/nl2br.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/nl2br.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/number_format.md b/docs/internals/builtins/string/number_format.md index 2356ac717f..3d34747829 100644 --- a/docs/internals/builtins/string/number_format.md +++ b/docs/internals/builtins/string/number_format.md @@ -2,7 +2,7 @@ title: "number_format() — internals" description: "Compiler internals for number_format(): lowering path, type checks, and runtime helpers." sidebar: - order: 393 + order: 400 --- ## `number_format()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/number_format.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/number_format.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/ord.md b/docs/internals/builtins/string/ord.md index 1d02db006a..a1b0d4fd69 100644 --- a/docs/internals/builtins/string/ord.md +++ b/docs/internals/builtins/string/ord.md @@ -2,7 +2,7 @@ title: "ord() — internals" description: "Compiler internals for ord(): lowering path, type checks, and runtime helpers." sidebar: - order: 394 + order: 401 --- ## `ord()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/ord.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ord.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/printf.md b/docs/internals/builtins/string/printf.md index 62ae929f25..66e79ecaca 100644 --- a/docs/internals/builtins/string/printf.md +++ b/docs/internals/builtins/string/printf.md @@ -2,7 +2,7 @@ title: "printf() — internals" description: "Compiler internals for printf(): lowering path, type checks, and runtime helpers." sidebar: - order: 395 + order: 402 --- ## `printf()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/printf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/printf.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/rawurldecode.md b/docs/internals/builtins/string/rawurldecode.md index b00a55c80a..3f391e357b 100644 --- a/docs/internals/builtins/string/rawurldecode.md +++ b/docs/internals/builtins/string/rawurldecode.md @@ -2,7 +2,7 @@ title: "rawurldecode() — internals" description: "Compiler internals for rawurldecode(): lowering path, type checks, and runtime helpers." sidebar: - order: 396 + order: 403 --- ## `rawurldecode()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/rawurldecode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/rawurldecode.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/rawurlencode.md b/docs/internals/builtins/string/rawurlencode.md index 9c302d9125..4e023017a8 100644 --- a/docs/internals/builtins/string/rawurlencode.md +++ b/docs/internals/builtins/string/rawurlencode.md @@ -2,7 +2,7 @@ title: "rawurlencode() — internals" description: "Compiler internals for rawurlencode(): lowering path, type checks, and runtime helpers." sidebar: - order: 397 + order: 404 --- ## `rawurlencode()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/rawurlencode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/rawurlencode.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/rtrim.md b/docs/internals/builtins/string/rtrim.md index 45c1f7eac6..dfe47bec5c 100644 --- a/docs/internals/builtins/string/rtrim.md +++ b/docs/internals/builtins/string/rtrim.md @@ -2,7 +2,7 @@ title: "rtrim() — internals" description: "Compiler internals for rtrim(): lowering path, type checks, and runtime helpers." sidebar: - order: 398 + order: 405 --- ## `rtrim()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/rtrim.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/rtrim.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/sha1.md b/docs/internals/builtins/string/sha1.md index f6fe7a6f5e..594522fd6a 100644 --- a/docs/internals/builtins/string/sha1.md +++ b/docs/internals/builtins/string/sha1.md @@ -2,7 +2,7 @@ title: "sha1() — internals" description: "Compiler internals for sha1(): lowering path, type checks, and runtime helpers." sidebar: - order: 399 + order: 406 --- ## `sha1()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/sha1.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/sha1.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/sprintf.md b/docs/internals/builtins/string/sprintf.md index 27abb545a7..363175007e 100644 --- a/docs/internals/builtins/string/sprintf.md +++ b/docs/internals/builtins/string/sprintf.md @@ -2,7 +2,7 @@ title: "sprintf() — internals" description: "Compiler internals for sprintf(): lowering path, type checks, and runtime helpers." sidebar: - order: 400 + order: 407 --- ## `sprintf()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/sprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/sprintf.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/sscanf.md b/docs/internals/builtins/string/sscanf.md index 76437c7875..68b35d970c 100644 --- a/docs/internals/builtins/string/sscanf.md +++ b/docs/internals/builtins/string/sscanf.md @@ -2,7 +2,7 @@ title: "sscanf() — internals" description: "Compiler internals for sscanf(): lowering path, type checks, and runtime helpers." sidebar: - order: 401 + order: 408 --- ## `sscanf()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/sscanf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/sscanf.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/str_contains.md b/docs/internals/builtins/string/str_contains.md index b791218bb0..a86187be16 100644 --- a/docs/internals/builtins/string/str_contains.md +++ b/docs/internals/builtins/string/str_contains.md @@ -2,7 +2,7 @@ title: "str_contains() — internals" description: "Compiler internals for str_contains(): lowering path, type checks, and runtime helpers." sidebar: - order: 402 + order: 409 --- ## `str_contains()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/str_contains.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_contains.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/str_ends_with.md b/docs/internals/builtins/string/str_ends_with.md index 7af5f7dc10..aea1ad8508 100644 --- a/docs/internals/builtins/string/str_ends_with.md +++ b/docs/internals/builtins/string/str_ends_with.md @@ -2,7 +2,7 @@ title: "str_ends_with() — internals" description: "Compiler internals for str_ends_with(): lowering path, type checks, and runtime helpers." sidebar: - order: 403 + order: 410 --- ## `str_ends_with()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/str_ends_with.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_ends_with.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/str_ireplace.md b/docs/internals/builtins/string/str_ireplace.md index df14bd6877..7ed5bf67d0 100644 --- a/docs/internals/builtins/string/str_ireplace.md +++ b/docs/internals/builtins/string/str_ireplace.md @@ -2,7 +2,7 @@ title: "str_ireplace() — internals" description: "Compiler internals for str_ireplace(): lowering path, type checks, and runtime helpers." sidebar: - order: 404 + order: 411 --- ## `str_ireplace()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/str_ireplace.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_ireplace.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/str_pad.md b/docs/internals/builtins/string/str_pad.md index 86cbb7df1c..a2e2772846 100644 --- a/docs/internals/builtins/string/str_pad.md +++ b/docs/internals/builtins/string/str_pad.md @@ -2,7 +2,7 @@ title: "str_pad() — internals" description: "Compiler internals for str_pad(): lowering path, type checks, and runtime helpers." sidebar: - order: 405 + order: 412 --- ## `str_pad()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/str_pad.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_pad.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/str_repeat.md b/docs/internals/builtins/string/str_repeat.md index c844533ec7..65bc6103a9 100644 --- a/docs/internals/builtins/string/str_repeat.md +++ b/docs/internals/builtins/string/str_repeat.md @@ -2,7 +2,7 @@ title: "str_repeat() — internals" description: "Compiler internals for str_repeat(): lowering path, type checks, and runtime helpers." sidebar: - order: 406 + order: 413 --- ## `str_repeat()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/str_repeat.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_repeat.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/str_replace.md b/docs/internals/builtins/string/str_replace.md index 8424c64b36..bf37573b1f 100644 --- a/docs/internals/builtins/string/str_replace.md +++ b/docs/internals/builtins/string/str_replace.md @@ -2,7 +2,7 @@ title: "str_replace() — internals" description: "Compiler internals for str_replace(): lowering path, type checks, and runtime helpers." sidebar: - order: 407 + order: 414 --- ## `str_replace()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/str_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_replace.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/str_split.md b/docs/internals/builtins/string/str_split.md index 456dbd9d56..d0ffc9ae3c 100644 --- a/docs/internals/builtins/string/str_split.md +++ b/docs/internals/builtins/string/str_split.md @@ -2,7 +2,7 @@ title: "str_split() — internals" description: "Compiler internals for str_split(): lowering path, type checks, and runtime helpers." sidebar: - order: 408 + order: 415 --- ## `str_split()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/str_split.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_split.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/str_starts_with.md b/docs/internals/builtins/string/str_starts_with.md index 98f8c92b5e..352123c5ff 100644 --- a/docs/internals/builtins/string/str_starts_with.md +++ b/docs/internals/builtins/string/str_starts_with.md @@ -2,7 +2,7 @@ title: "str_starts_with() — internals" description: "Compiler internals for str_starts_with(): lowering path, type checks, and runtime helpers." sidebar: - order: 409 + order: 416 --- ## `str_starts_with()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/str_starts_with.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_starts_with.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/strcasecmp.md b/docs/internals/builtins/string/strcasecmp.md index 0cb25f5ed6..8bd8506ddd 100644 --- a/docs/internals/builtins/string/strcasecmp.md +++ b/docs/internals/builtins/string/strcasecmp.md @@ -2,7 +2,7 @@ title: "strcasecmp() — internals" description: "Compiler internals for strcasecmp(): lowering path, type checks, and runtime helpers." sidebar: - order: 410 + order: 417 --- ## `strcasecmp()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/strcasecmp.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strcasecmp.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/strcmp.md b/docs/internals/builtins/string/strcmp.md index 358cd9e9d7..5e43e4832c 100644 --- a/docs/internals/builtins/string/strcmp.md +++ b/docs/internals/builtins/string/strcmp.md @@ -2,7 +2,7 @@ title: "strcmp() — internals" description: "Compiler internals for strcmp(): lowering path, type checks, and runtime helpers." sidebar: - order: 411 + order: 418 --- ## `strcmp()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/strcmp.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strcmp.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/stripslashes.md b/docs/internals/builtins/string/stripslashes.md index dfd133a06c..67ddefe876 100644 --- a/docs/internals/builtins/string/stripslashes.md +++ b/docs/internals/builtins/string/stripslashes.md @@ -2,7 +2,7 @@ title: "stripslashes() — internals" description: "Compiler internals for stripslashes(): lowering path, type checks, and runtime helpers." sidebar: - order: 412 + order: 419 --- ## `stripslashes()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/stripslashes.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/stripslashes.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/strlen.md b/docs/internals/builtins/string/strlen.md index f15dffa9ae..16ac5c8b72 100644 --- a/docs/internals/builtins/string/strlen.md +++ b/docs/internals/builtins/string/strlen.md @@ -2,7 +2,7 @@ title: "strlen() — internals" description: "Compiler internals for strlen(): lowering path, type checks, and runtime helpers." sidebar: - order: 413 + order: 420 --- ## `strlen()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/strlen.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strlen.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/strpos.md b/docs/internals/builtins/string/strpos.md index ed093374a4..69259e59c5 100644 --- a/docs/internals/builtins/string/strpos.md +++ b/docs/internals/builtins/string/strpos.md @@ -2,7 +2,7 @@ title: "strpos() — internals" description: "Compiler internals for strpos(): lowering path, type checks, and runtime helpers." sidebar: - order: 414 + order: 421 --- ## `strpos()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/strpos.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strpos.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/strrev.md b/docs/internals/builtins/string/strrev.md index d005686011..de6bccb986 100644 --- a/docs/internals/builtins/string/strrev.md +++ b/docs/internals/builtins/string/strrev.md @@ -2,7 +2,7 @@ title: "strrev() — internals" description: "Compiler internals for strrev(): lowering path, type checks, and runtime helpers." sidebar: - order: 415 + order: 422 --- ## `strrev()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/strrev.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strrev.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/strrpos.md b/docs/internals/builtins/string/strrpos.md index a269a6eda1..60838e68c2 100644 --- a/docs/internals/builtins/string/strrpos.md +++ b/docs/internals/builtins/string/strrpos.md @@ -2,7 +2,7 @@ title: "strrpos() — internals" description: "Compiler internals for strrpos(): lowering path, type checks, and runtime helpers." sidebar: - order: 416 + order: 423 --- ## `strrpos()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/strrpos.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strrpos.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/strstr.md b/docs/internals/builtins/string/strstr.md index 80b8148e74..0db4a77a0b 100644 --- a/docs/internals/builtins/string/strstr.md +++ b/docs/internals/builtins/string/strstr.md @@ -2,7 +2,7 @@ title: "strstr() — internals" description: "Compiler internals for strstr(): lowering path, type checks, and runtime helpers." sidebar: - order: 417 + order: 424 --- ## `strstr()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/strstr.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strstr.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/strtolower.md b/docs/internals/builtins/string/strtolower.md index 58a3d9055c..de1403ee2a 100644 --- a/docs/internals/builtins/string/strtolower.md +++ b/docs/internals/builtins/string/strtolower.md @@ -2,7 +2,7 @@ title: "strtolower() — internals" description: "Compiler internals for strtolower(): lowering path, type checks, and runtime helpers." sidebar: - order: 418 + order: 425 --- ## `strtolower()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/strtolower.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strtolower.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/strtoupper.md b/docs/internals/builtins/string/strtoupper.md index bf0b9ad5ac..ce25e6a59f 100644 --- a/docs/internals/builtins/string/strtoupper.md +++ b/docs/internals/builtins/string/strtoupper.md @@ -2,7 +2,7 @@ title: "strtoupper() — internals" description: "Compiler internals for strtoupper(): lowering path, type checks, and runtime helpers." sidebar: - order: 419 + order: 426 --- ## `strtoupper()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/strtoupper.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strtoupper.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/substr.md b/docs/internals/builtins/string/substr.md index 575bc0f347..5fda6638b2 100644 --- a/docs/internals/builtins/string/substr.md +++ b/docs/internals/builtins/string/substr.md @@ -2,7 +2,7 @@ title: "substr() — internals" description: "Compiler internals for substr(): lowering path, type checks, and runtime helpers." sidebar: - order: 420 + order: 427 --- ## `substr()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/substr.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/substr.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/substr_replace.md b/docs/internals/builtins/string/substr_replace.md index dd009a9934..9390e52165 100644 --- a/docs/internals/builtins/string/substr_replace.md +++ b/docs/internals/builtins/string/substr_replace.md @@ -2,7 +2,7 @@ title: "substr_replace() — internals" description: "Compiler internals for substr_replace(): lowering path, type checks, and runtime helpers." sidebar: - order: 421 + order: 428 --- ## `substr_replace()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/substr_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/substr_replace.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/trim.md b/docs/internals/builtins/string/trim.md index 646be2d5f6..4ad375842c 100644 --- a/docs/internals/builtins/string/trim.md +++ b/docs/internals/builtins/string/trim.md @@ -2,7 +2,7 @@ title: "trim() — internals" description: "Compiler internals for trim(): lowering path, type checks, and runtime helpers." sidebar: - order: 422 + order: 429 --- ## `trim()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/trim.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/trim.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/ucfirst.md b/docs/internals/builtins/string/ucfirst.md index caaf2ccd7c..71766305d3 100644 --- a/docs/internals/builtins/string/ucfirst.md +++ b/docs/internals/builtins/string/ucfirst.md @@ -2,7 +2,7 @@ title: "ucfirst() — internals" description: "Compiler internals for ucfirst(): lowering path, type checks, and runtime helpers." sidebar: - order: 423 + order: 430 --- ## `ucfirst()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/ucfirst.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ucfirst.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/ucwords.md b/docs/internals/builtins/string/ucwords.md index c7572607cf..a466f97bad 100644 --- a/docs/internals/builtins/string/ucwords.md +++ b/docs/internals/builtins/string/ucwords.md @@ -2,7 +2,7 @@ title: "ucwords() — internals" description: "Compiler internals for ucwords(): lowering path, type checks, and runtime helpers." sidebar: - order: 424 + order: 431 --- ## `ucwords()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/ucwords.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ucwords.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/urldecode.md b/docs/internals/builtins/string/urldecode.md index a9fb40160a..4a76bd4d72 100644 --- a/docs/internals/builtins/string/urldecode.md +++ b/docs/internals/builtins/string/urldecode.md @@ -2,7 +2,7 @@ title: "urldecode() — internals" description: "Compiler internals for urldecode(): lowering path, type checks, and runtime helpers." sidebar: - order: 425 + order: 432 --- ## `urldecode()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/urldecode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/urldecode.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/urlencode.md b/docs/internals/builtins/string/urlencode.md index eb16c5cde2..64019bb41a 100644 --- a/docs/internals/builtins/string/urlencode.md +++ b/docs/internals/builtins/string/urlencode.md @@ -2,7 +2,7 @@ title: "urlencode() — internals" description: "Compiler internals for urlencode(): lowering path, type checks, and runtime helpers." sidebar: - order: 426 + order: 433 --- ## `urlencode()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/urlencode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/urlencode.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/vprintf.md b/docs/internals/builtins/string/vprintf.md index e5e18bdd5f..d9c88472e4 100644 --- a/docs/internals/builtins/string/vprintf.md +++ b/docs/internals/builtins/string/vprintf.md @@ -2,7 +2,7 @@ title: "vprintf() — internals" description: "Compiler internals for vprintf(): lowering path, type checks, and runtime helpers." sidebar: - order: 427 + order: 434 --- ## `vprintf()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/vprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/vprintf.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/vsprintf.md b/docs/internals/builtins/string/vsprintf.md index 1966cc7a54..33c98c9d92 100644 --- a/docs/internals/builtins/string/vsprintf.md +++ b/docs/internals/builtins/string/vsprintf.md @@ -2,7 +2,7 @@ title: "vsprintf() — internals" description: "Compiler internals for vsprintf(): lowering path, type checks, and runtime helpers." sidebar: - order: 428 + order: 435 --- ## `vsprintf()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/vsprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/vsprintf.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/wordwrap.md b/docs/internals/builtins/string/wordwrap.md index 6c88933c5a..0d3fc388e4 100644 --- a/docs/internals/builtins/string/wordwrap.md +++ b/docs/internals/builtins/string/wordwrap.md @@ -2,7 +2,7 @@ title: "wordwrap() — internals" description: "Compiler internals for wordwrap(): lowering path, type checks, and runtime helpers." sidebar: - order: 429 + order: 436 --- ## `wordwrap()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/wordwrap.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/wordwrap.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/boolval.md b/docs/internals/builtins/type/boolval.md index d028641b8d..71739549a6 100644 --- a/docs/internals/builtins/type/boolval.md +++ b/docs/internals/builtins/type/boolval.md @@ -2,7 +2,7 @@ title: "boolval() — internals" description: "Compiler internals for boolval(): lowering path, type checks, and runtime helpers." sidebar: - order: 430 + order: 437 --- ## `boolval()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/boolval.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/boolval.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/ctype_alnum.md b/docs/internals/builtins/type/ctype_alnum.md index a23ba0bfa6..0aa1d29d43 100644 --- a/docs/internals/builtins/type/ctype_alnum.md +++ b/docs/internals/builtins/type/ctype_alnum.md @@ -2,7 +2,7 @@ title: "ctype_alnum() — internals" description: "Compiler internals for ctype_alnum(): lowering path, type checks, and runtime helpers." sidebar: - order: 431 + order: 438 --- ## `ctype_alnum()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/ctype_alnum.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ctype_alnum.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/ctype_alpha.md b/docs/internals/builtins/type/ctype_alpha.md index b3113bed86..022660c63a 100644 --- a/docs/internals/builtins/type/ctype_alpha.md +++ b/docs/internals/builtins/type/ctype_alpha.md @@ -2,7 +2,7 @@ title: "ctype_alpha() — internals" description: "Compiler internals for ctype_alpha(): lowering path, type checks, and runtime helpers." sidebar: - order: 432 + order: 439 --- ## `ctype_alpha()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/ctype_alpha.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ctype_alpha.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/ctype_digit.md b/docs/internals/builtins/type/ctype_digit.md index f204f377e0..2b9467d7af 100644 --- a/docs/internals/builtins/type/ctype_digit.md +++ b/docs/internals/builtins/type/ctype_digit.md @@ -2,7 +2,7 @@ title: "ctype_digit() — internals" description: "Compiler internals for ctype_digit(): lowering path, type checks, and runtime helpers." sidebar: - order: 433 + order: 440 --- ## `ctype_digit()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/ctype_digit.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ctype_digit.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/ctype_space.md b/docs/internals/builtins/type/ctype_space.md index 2546a516e1..b60256c008 100644 --- a/docs/internals/builtins/type/ctype_space.md +++ b/docs/internals/builtins/type/ctype_space.md @@ -2,7 +2,7 @@ title: "ctype_space() — internals" description: "Compiler internals for ctype_space(): lowering path, type checks, and runtime helpers." sidebar: - order: 434 + order: 441 --- ## `ctype_space()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/ctype_space.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ctype_space.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/floatval.md b/docs/internals/builtins/type/floatval.md index a7a2289880..30fe624229 100644 --- a/docs/internals/builtins/type/floatval.md +++ b/docs/internals/builtins/type/floatval.md @@ -2,7 +2,7 @@ title: "floatval() — internals" description: "Compiler internals for floatval(): lowering path, type checks, and runtime helpers." sidebar: - order: 435 + order: 442 --- ## `floatval()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/floatval.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/floatval.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/get_resource_id.md b/docs/internals/builtins/type/get_resource_id.md index aebb557ce1..af2767f276 100644 --- a/docs/internals/builtins/type/get_resource_id.md +++ b/docs/internals/builtins/type/get_resource_id.md @@ -2,7 +2,7 @@ title: "get_resource_id() — internals" description: "Compiler internals for get_resource_id(): lowering path, type checks, and runtime helpers." sidebar: - order: 436 + order: 443 --- ## `get_resource_id()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/get_resource_id.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/get_resource_id.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/get_resource_type.md b/docs/internals/builtins/type/get_resource_type.md index 38027d7d9f..86fed98243 100644 --- a/docs/internals/builtins/type/get_resource_type.md +++ b/docs/internals/builtins/type/get_resource_type.md @@ -2,7 +2,7 @@ title: "get_resource_type() — internals" description: "Compiler internals for get_resource_type(): lowering path, type checks, and runtime helpers." sidebar: - order: 437 + order: 444 --- ## `get_resource_type()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/get_resource_type.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/get_resource_type.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/gettype.md b/docs/internals/builtins/type/gettype.md index 271b04da86..66ef119f5d 100644 --- a/docs/internals/builtins/type/gettype.md +++ b/docs/internals/builtins/type/gettype.md @@ -2,7 +2,7 @@ title: "gettype() — internals" description: "Compiler internals for gettype(): lowering path, type checks, and runtime helpers." sidebar: - order: 438 + order: 445 --- ## `gettype()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/gettype.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/gettype.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/intval.md b/docs/internals/builtins/type/intval.md index 9e2012a5cc..c77dc6f41c 100644 --- a/docs/internals/builtins/type/intval.md +++ b/docs/internals/builtins/type/intval.md @@ -2,7 +2,7 @@ title: "intval() — internals" description: "Compiler internals for intval(): lowering path, type checks, and runtime helpers." sidebar: - order: 439 + order: 446 --- ## `intval()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/intval.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/intval.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_array.md b/docs/internals/builtins/type/is_array.md index 510bcf4000..9e8fb60fa6 100644 --- a/docs/internals/builtins/type/is_array.md +++ b/docs/internals/builtins/type/is_array.md @@ -2,7 +2,7 @@ title: "is_array() — internals" description: "Compiler internals for is_array(): lowering path, type checks, and runtime helpers." sidebar: - order: 440 + order: 447 --- ## `is_array()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_array.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_array.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_bool.md b/docs/internals/builtins/type/is_bool.md index ba40f9c0d9..fffb9a12d7 100644 --- a/docs/internals/builtins/type/is_bool.md +++ b/docs/internals/builtins/type/is_bool.md @@ -2,7 +2,7 @@ title: "is_bool() — internals" description: "Compiler internals for is_bool(): lowering path, type checks, and runtime helpers." sidebar: - order: 441 + order: 448 --- ## `is_bool()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_bool.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_bool.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_callable.md b/docs/internals/builtins/type/is_callable.md index 5436f18679..1ea05ac1e8 100644 --- a/docs/internals/builtins/type/is_callable.md +++ b/docs/internals/builtins/type/is_callable.md @@ -2,7 +2,7 @@ title: "is_callable() — internals" description: "Compiler internals for is_callable(): lowering path, type checks, and runtime helpers." sidebar: - order: 442 + order: 449 --- ## `is_callable()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_callable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_callable.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_double.md b/docs/internals/builtins/type/is_double.md index 6c1c22ea31..7c2df41dd2 100644 --- a/docs/internals/builtins/type/is_double.md +++ b/docs/internals/builtins/type/is_double.md @@ -2,7 +2,7 @@ title: "is_double() — internals" description: "Compiler internals for is_double(): lowering path, type checks, and runtime helpers." sidebar: - order: 443 + order: 450 --- ## `is_double()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_double.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_double.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_float.md b/docs/internals/builtins/type/is_float.md index 3fbf2e9671..ac358b7808 100644 --- a/docs/internals/builtins/type/is_float.md +++ b/docs/internals/builtins/type/is_float.md @@ -2,7 +2,7 @@ title: "is_float() — internals" description: "Compiler internals for is_float(): lowering path, type checks, and runtime helpers." sidebar: - order: 444 + order: 451 --- ## `is_float()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_float.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_float.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_int.md b/docs/internals/builtins/type/is_int.md index 06216d6dbb..6d9e7f952a 100644 --- a/docs/internals/builtins/type/is_int.md +++ b/docs/internals/builtins/type/is_int.md @@ -2,7 +2,7 @@ title: "is_int() — internals" description: "Compiler internals for is_int(): lowering path, type checks, and runtime helpers." sidebar: - order: 445 + order: 452 --- ## `is_int()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_int.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_int.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_integer.md b/docs/internals/builtins/type/is_integer.md index 8994908c42..d8409ddc8f 100644 --- a/docs/internals/builtins/type/is_integer.md +++ b/docs/internals/builtins/type/is_integer.md @@ -2,7 +2,7 @@ title: "is_integer() — internals" description: "Compiler internals for is_integer(): lowering path, type checks, and runtime helpers." sidebar: - order: 446 + order: 453 --- ## `is_integer()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_integer.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_integer.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_iterable.md b/docs/internals/builtins/type/is_iterable.md index 350434185f..b3e49cd814 100644 --- a/docs/internals/builtins/type/is_iterable.md +++ b/docs/internals/builtins/type/is_iterable.md @@ -2,7 +2,7 @@ title: "is_iterable() — internals" description: "Compiler internals for is_iterable(): lowering path, type checks, and runtime helpers." sidebar: - order: 447 + order: 454 --- ## `is_iterable()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_iterable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_iterable.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_long.md b/docs/internals/builtins/type/is_long.md index 8f62d731aa..ea44a73426 100644 --- a/docs/internals/builtins/type/is_long.md +++ b/docs/internals/builtins/type/is_long.md @@ -2,7 +2,7 @@ title: "is_long() — internals" description: "Compiler internals for is_long(): lowering path, type checks, and runtime helpers." sidebar: - order: 448 + order: 455 --- ## `is_long()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_long.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_long.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_null.md b/docs/internals/builtins/type/is_null.md index fae355433a..4c679c5f22 100644 --- a/docs/internals/builtins/type/is_null.md +++ b/docs/internals/builtins/type/is_null.md @@ -2,7 +2,7 @@ title: "is_null() — internals" description: "Compiler internals for is_null(): lowering path, type checks, and runtime helpers." sidebar: - order: 449 + order: 456 --- ## `is_null()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_null.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_null.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_numeric.md b/docs/internals/builtins/type/is_numeric.md index e7c6840763..dcd50be433 100644 --- a/docs/internals/builtins/type/is_numeric.md +++ b/docs/internals/builtins/type/is_numeric.md @@ -2,7 +2,7 @@ title: "is_numeric() — internals" description: "Compiler internals for is_numeric(): lowering path, type checks, and runtime helpers." sidebar: - order: 450 + order: 457 --- ## `is_numeric()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_numeric.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_numeric.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_object.md b/docs/internals/builtins/type/is_object.md index 609c9f460b..543b97381c 100644 --- a/docs/internals/builtins/type/is_object.md +++ b/docs/internals/builtins/type/is_object.md @@ -2,7 +2,7 @@ title: "is_object() — internals" description: "Compiler internals for is_object(): lowering path, type checks, and runtime helpers." sidebar: - order: 451 + order: 458 --- ## `is_object()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_object.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_object.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_real.md b/docs/internals/builtins/type/is_real.md index fdcd64e6f7..982608dbe5 100644 --- a/docs/internals/builtins/type/is_real.md +++ b/docs/internals/builtins/type/is_real.md @@ -2,7 +2,7 @@ title: "is_real() — internals" description: "Compiler internals for is_real(): lowering path, type checks, and runtime helpers." sidebar: - order: 452 + order: 459 --- ## `is_real()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_real.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_real.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_resource.md b/docs/internals/builtins/type/is_resource.md index cffd479880..53a7ec7fe3 100644 --- a/docs/internals/builtins/type/is_resource.md +++ b/docs/internals/builtins/type/is_resource.md @@ -2,7 +2,7 @@ title: "is_resource() — internals" description: "Compiler internals for is_resource(): lowering path, type checks, and runtime helpers." sidebar: - order: 453 + order: 460 --- ## `is_resource()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_resource.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_resource.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_scalar.md b/docs/internals/builtins/type/is_scalar.md index ec678e5f30..5d15f4424c 100644 --- a/docs/internals/builtins/type/is_scalar.md +++ b/docs/internals/builtins/type/is_scalar.md @@ -2,7 +2,7 @@ title: "is_scalar() — internals" description: "Compiler internals for is_scalar(): lowering path, type checks, and runtime helpers." sidebar: - order: 454 + order: 461 --- ## `is_scalar()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_scalar.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_scalar.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_string.md b/docs/internals/builtins/type/is_string.md index fef2821e30..d0b1803db8 100644 --- a/docs/internals/builtins/type/is_string.md +++ b/docs/internals/builtins/type/is_string.md @@ -2,7 +2,7 @@ title: "is_string() — internals" description: "Compiler internals for is_string(): lowering path, type checks, and runtime helpers." sidebar: - order: 455 + order: 462 --- ## `is_string()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_string.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_string.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/settype.md b/docs/internals/builtins/type/settype.md index da88946bcb..334f96f777 100644 --- a/docs/internals/builtins/type/settype.md +++ b/docs/internals/builtins/type/settype.md @@ -2,7 +2,7 @@ title: "settype() — internals" description: "Compiler internals for settype(): lowering path, type checks, and runtime helpers." sidebar: - order: 456 + order: 463 --- ## `settype()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/settype.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/settype.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,7 +39,7 @@ sidebar: ## Signature summary ```php -function settype(mixed $var, string $type): bool +function settype(mixed &$var, string $type): bool ``` ## What the type checker enforces diff --git a/docs/internals/builtins/type/strval.md b/docs/internals/builtins/type/strval.md index b20c15049a..e8616cdfa3 100644 --- a/docs/internals/builtins/type/strval.md +++ b/docs/internals/builtins/type/strval.md @@ -2,7 +2,7 @@ title: "strval() — internals" description: "Compiler internals for strval(): lowering path, type checks, and runtime helpers." sidebar: - order: 457 + order: 464 --- ## `strval()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/strval.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/strval.rs) -- **Lowering**: [`src/builtins/semantics.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L423) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:426](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L426) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/memory-model.md b/docs/internals/memory-model.md index dd2145e8da..6db37a180f 100644 --- a/docs/internals/memory-model.md +++ b/docs/internals/memory-model.md @@ -45,6 +45,8 @@ This page explains where every value lives in memory at runtime. │ │ _json_indent_depth, _json_validate_*, │ │ _json_decode_assoc, _json_error_*, │ │ _fiber_current, _fiber_main_saved_*, +│ │ _proc_pipe_registry_head, +│ │ _proc_status_registry_head, │ │ _generator_class_id, │ │ _print_r_mode/_print_r_off, │ │ _ob_level/_ob_in_handler/_ob_flushing, @@ -170,9 +172,9 @@ Pointers are stored as raw 64-bit addresses. An opaque pointer and a typed `ptr< ### Fiber stacks and scheduler state -`Fiber` objects own native stacks rather than borrowing the caller's stack. The runtime allocates each fiber stack through `mmap` (256 KiB usable by default, plus the guard page), protects the bottom 16 KiB with `mprotect(PROT_NONE)` as a guard page, and stores both the mapping base and total mapped size in the Fiber object so `__rt_fiber_free_stack` can later return it with `munmap`. +`Fiber` objects own native stacks rather than borrowing the caller's stack. The runtime allocates each fiber stack through `mmap` on Unix or the Win32 `VirtualAlloc` shim (256 KiB usable by default, plus the guard page), protects the bottom 16 KiB with `mprotect(PROT_NONE)` or `VirtualProtect(PAGE_NOACCESS)`, and stores both the mapping base and total mapped size in the Fiber object so `__rt_fiber_free_stack` can later return it through `munmap` or `VirtualFree`. -The currently running fiber is tracked in `_fiber_current`. When execution switches away from the main stack, `_fiber_main_saved_sp`, `_fiber_main_saved_exc`, and `_fiber_main_saved_call_frame` preserve the main stack pointer, exception-handler chain, and activation-record cleanup chain. A suspended Fiber stores the same state inside its object payload (`saved_sp`, `own_exc_head`, and `own_call_frame`), so `__rt_fiber_switch` can swap between main and fiber contexts without mixing exception or cleanup chains. +The currently running fiber is tracked in `_fiber_current`. When execution switches away from the main stack, `_fiber_main_saved_sp`, `_fiber_main_saved_exc`, and `_fiber_main_saved_call_frame` preserve the main stack pointer, exception-handler chain, and activation-record cleanup chain. A suspended Fiber stores the same state inside its object payload (`saved_sp`, `own_exc_head`, and `own_call_frame`), so `__rt_fiber_switch` can swap between main and fiber contexts without mixing exception or cleanup chains. On Windows x86_64, three additional main-stack slots preserve the TEB `StackBase`, `StackLimit`, and `DeallocationStack` values; the switch updates those TEB fields for the active fiber stack and restores them before resuming the main stack. ## The string buffer (scratch pad) @@ -512,6 +514,7 @@ The runtime data layer is split into fixed shared data, user-program data, and d - `_heap_err_msg`, `_arr_cap_err_msg`, `_ptr_null_err_msg` — fatal runtime error strings - `_buffer_bounds_msg`, `_buffer_uaf_msg`, `_match_unhandled_msg`, `_static_prop_private_access_msg`, `_instanceof_target_type_msg`, `_iterable_unsupported_kind_msg` — fatal runtime error strings for buffers, `match`, late-bound private static-property access, dynamic `instanceof` target validation, and iterable dispatch - `_fiber_msg_*` — Fiber state-error message strings used when constructing `FiberError` +- `_proc_pipe_registry_head`, `_proc_status_registry_head` — roots for heap-backed process pipe and process status registries - `_rt_diag_suppression`, `_diag_fopen_failed_msg`, `_diag_file_get_contents_failed_msg`, `_diag_define_already_defined_msg` — runtime warning suppression depth and warning strings used by `@` - `_resource_id_prefix` — prefix used by resource display helpers - `_php_uname_mode_len_msg`, `_php_uname_mode_value_msg` — fatal `php_uname()` diagnostics for invalid mode arguments @@ -597,7 +600,8 @@ The naming pattern comes from `static_property_symbol(...)`. Inherited static pr | Heap | 8MB (configurable) | Fatal error: "heap memory exhausted" | | Heap metadata | `_heap_off`, `_heap_free_list`, `_heap_small_bins`, `_heap_debug_enabled`, `_gc_*` flags/counters = 104 bytes total | Fixed-size bookkeeping, not user-visible | | Exception state | `_exc_handler_top`, `_exc_call_frame_top`, `_exc_value` = 24 bytes total | Fixed-size setjmp/longjmp handler and thrown-value bookkeeping | -| 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 | +| Fiber scheduler state | 32 bytes on Unix; 56 bytes on Windows (`_fiber_current`, main SP/exception/call-frame state, plus the three saved main-stack TEB words) | Fixed-size current-fiber and main-frame resume bookkeeping | +| Process registries | `_proc_pipe_registry_head`, `_proc_status_registry_head` = 16 bytes total, with entries allocated from the managed heap | Retained pipe containers and process metadata are released when `proc_close()` consumes the process resource | | 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) | diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 36a3a89150..8756d41936 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -5,7 +5,7 @@ sidebar: order: 8 --- -**Source:** `src/codegen_support/runtime/` — `mod.rs`, `emitters.rs`, `diagnostics.rs`, `data/`, `strings/`, `arrays/`, `buffers/`, `callables/`, `exceptions.rs`, `exceptions/`, `io/`, `objects/`, `spl/`, `system/`, `pointers/`, `zval/`, `fibers/`, `generators/`, plus the eval hooks `eval_bridge.rs` / `eval_scope.rs` +**Source:** `src/codegen_support/runtime/` — `mod.rs`, `emitters.rs`, `diagnostics.rs`, `data/`, `strings/`, `arrays/`, `buffers/`, `callables/`, `exceptions.rs`, `exceptions/`, `io/`, `objects/`, `spl/`, `system/`, `pointers/`, `zval/`, `fibers/`, `generators/`, `win32/`, plus the eval hooks `eval_bridge.rs` / `eval_scope.rs` The runtime is a collection of **hand-written assembly routines** that handle operations too complex for inline code generation. When the [code generator](the-codegen.md) needs to convert an integer to a string or concatenate two strings, it emits a `bl __rt_itoa` or `bl __rt_concat` — a call to a runtime routine. @@ -182,6 +182,9 @@ Each routine follows the same pattern — inputs in registers, output in standar | `__rt_str_ends_with` | Check suffix match | `x1`/`x2` + `x3`/`x4` | `x0` (0 or 1) | | `__rt_chr` | ASCII code → char | `x0` | `x1`/`x2` | | `__rt_addslashes` | Escape quotes/backslashes | `x1`/`x2` | `x1`/`x2` | +| `__rt_escapeshellarg` | Quote one shell argument with PHP's POSIX or Windows `cmd.exe` rules; NUL and Windows length violations throw `ValueError` | `x1`/`x2` | `x1`/`x2` | +| `__rt_escapeshellcmd` | Escape PHP's platform-specific shell metacharacter set; NUL and Windows length violations throw `ValueError` | `x1`/`x2` | `x1`/`x2` | +| `__rt_shell_utf8_sequence_len` | Validate the next bounded UTF-8 sequence so multibyte continuation bytes are copied atomically instead of interpreted as shell syntax | source pointer + remaining length | sequence width, or 0 for an invalid leading byte | | `__rt_nl2br` | Insert `
` before newlines | `x1`/`x2` | `x1`/`x2` | | `__rt_bin2hex` | Binary → hex string | `x1`/`x2` | `x1`/`x2` | | `__rt_hex2bin` | Hex → binary | `x1`/`x2` | `x1`/`x2` | @@ -246,7 +249,7 @@ Extern callback trampolines use the same descriptor invoker from a C-facing entr ## Array routines -**Source:** `src/codegen_support/runtime/arrays/` (148 files) +**Source:** `src/codegen_support/runtime/arrays/` (149 files) ### Core allocation @@ -405,7 +408,7 @@ At program start, the OS passes `argc` (argument count) in `x0` and `argv` (poin ## Exception routines -**Source:** `src/codegen_support/runtime/exceptions.rs` plus `src/codegen_support/runtime/exceptions/` (6 files) +**Source:** `src/codegen_support/runtime/exceptions.rs` plus `src/codegen_support/runtime/exceptions/` (8 files) elephc lowers exceptions with a small runtime layer around `_setjmp` / `_longjmp`. Codegen publishes the current exception object into `_exc_value`, pushes a handler record into `_exc_handler_top`, and then uses these helpers to unwind, match catch clauses, and resume control flow through `catch` / `finally`. @@ -418,6 +421,7 @@ elephc lowers exceptions with a small runtime layer around `_setjmp` / `_longjmp | `__rt_class_implements_interface` | Test class metadata against an interface id for dynamic class-string checks without an object instance | `x0` = class id, `x1` = interface id | `x0` = 1 if implemented, 0 otherwise | | `__rt_throw_current` | Unwind to the nearest active handler or print the fatal uncaught-exception message and exit | reads `_exc_value`, `_exc_handler_top`, `_exc_call_frame_top` | does not return normally | | `__rt_rethrow_current` | Re-enter the ordinary throw path with the currently active exception | none (uses global exception state) | does not return normally | +| `__rt_throw_static_exception` | x86_64 helper that allocates a built-in Throwable from a static class id/message tuple in its own unwindable frame | class id + message pointer/length | does not return normally | The fatal uncaught-exception path writes `Fatal error: uncaught exception` to stderr and exits with status 1. The runtime also resets the concat-buffer cursor before the final `longjmp`, so partially built string state from the throwing frame does not leak into the resumed catch/finally code. @@ -500,7 +504,7 @@ All regex routines use PCRE2 through the PCRE2 POSIX-compatible wrapper (`pcre2_ ## I/O routines -**Source:** `src/codegen_support/runtime/io/` (117 files) +**Source:** `src/codegen_support/runtime/io/` (123 files) These routines handle file and filesystem operations through target-aware libc/syscall helpers. PHP strings (pointer + length) must be converted to null-terminated C strings before passing to C or OS APIs — `__rt_cstr` handles the primary buffer and also emits `__rt_cstr2` for routines that need a second simultaneous C string. @@ -548,6 +552,18 @@ The first table covers the file/filesystem core; the subsections after it cover | `__rt_fsync` / `__rt_fflush` / `__rt_fdatasync` | File descriptor flush helpers; `fflush()` maps to `fsync()` because elephc has no userspace stdio buffer | | `__rt_touch` | Create missing files and update access/modification timestamps | +### Process routines + +| Routine | What it does | +|---|---| +| `__rt_proc_open` | Create a child process and materialize PHP descriptor specifications. Windows supports `pipe`, private loopback `socket`, `file`, `redirect`, supplied stream resources, and `null` descriptors through inheritable Win32 handles. | +| `__rt_proc_pipe_registry_register` / `__rt_proc_pipe_registry_close` | Retain the exact published `$pipes` container and close its still-live process pipes before `proc_close()` waits, preventing an unread full pipe from deadlocking the parent. | +| `__rt_proc_status_register` / `__rt_proc_status_register_cstr` / `__rt_proc_status_unregister` | Retain the command string and process metadata needed by `proc_get_status()` until `proc_close()` consumes the process resource. | +| `__rt_proc_status_cached_exit` | Return a normal Unix exit status already harvested by a non-blocking status query, so `proc_close()` does not wait for the same child twice. | +| `__rt_proc_get_status` | Build PHP's nine-field process-status hash. Unix uses non-blocking `wait4`; Windows uses `GetExitCodeProcess` and does not consume the process handle. | +| `__rt_proc_terminate` | Send the requested Unix signal, or call `TerminateProcess(handle, 255)` on Windows as php-src does. | +| `__rt_proc_close` | Close registered pipes, consume cached status if present, then wait/reap the child and release process metadata/handles. | + ### Stream and socket routines | Routine | What it does | @@ -790,6 +806,13 @@ Generators are stamped as object heap blocks (heap kind `4`) because `Generator` These helpers implement PHP 8.1-style cooperative coroutines. They are emitted by the shared runtime on every supported target. +The context switch follows the active target ABI: AArch64 and System V x86_64 +preserve their callee-saved register sets, while Windows x86_64 additionally +preserves `rsi`, `rdi`, and `xmm6`–`xmm15`. The Windows path also keeps the +thread environment block's `StackBase`, `StackLimit`, and `DeallocationStack` +fields synchronized with the active fiber stack so Win32 stack probing and +unwinding continue to see valid bounds. + | Routine | What it does | Input | Output | |---|---|---|---| | `__rt_fiber_alloc_stack` | Allocate a per-fiber native stack with a protected guard page | requested usable stack size | stack base, initial top, mapped size | @@ -876,6 +899,11 @@ The runtime data layer lives in `src/codegen_support/runtime/data/`. `fixed.rs` .comm _fiber_main_saved_sp, 8 ; saved main-stack pointer while running a fiber .comm _fiber_main_saved_exc, 8 ; saved main exception-handler chain while running a fiber .comm _fiber_main_saved_call_frame, 8 ; saved main cleanup-frame chain while running a fiber +.comm _fiber_main_saved_stack_base, 8 ; Windows: saved main TEB StackBase +.comm _fiber_main_saved_stack_limit, 8 ; Windows: saved main TEB StackLimit +.comm _fiber_main_saved_deallocation_stack, 8 ; Windows: saved main TEB DeallocationStack +.comm _proc_pipe_registry_head, 8 ; retained proc_open() pipe-container registry +.comm _proc_status_registry_head, 8 ; proc_get_status()/proc_close() metadata registry .comm _rt_diag_suppression, 8 ; nested runtime warning-suppression depth for @ .comm _heap_buf, 8388608 ; 8MB heap by default (--heap-size overrides) .comm _heap_off, 8 ; current heap offset diff --git a/docs/php/builtins.md b/docs/php/builtins.md index f8beb7a0c4..579a2c3545 100644 --- a/docs/php/builtins.md +++ b/docs/php/builtins.md @@ -33,45 +33,45 @@ sidebar: | [`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_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_pop()`](./builtins/array/array_pop.md) | `(array &$array): mixed` | `mixed` | ✓ | ✓ | | [`array_product()`](./builtins/array/array_product.md) | `(array $array): int` | `int` | ✓ | ✓ | -| [`array_push()`](./builtins/array/array_push.md) | `(array $array, ...$values): void` | `void` | ✓ | ✓ | +| [`array_push()`](./builtins/array/array_push.md) | `(array &$array, ...$values): void` | `void` | ✓ | ✓ | | [`array_rand()`](./builtins/array/array_rand.md) | `(array $array): int` | `int` | ✓ | ✓ | | [`array_reduce()`](./builtins/array/array_reduce.md) | `(array $array, callable $callback, mixed $initial = null): int` | `int` | ✓ | ✓ | | [`array_replace()`](./builtins/array/array_replace.md) | `(array $array, array $replacements): mixed` | `mixed` | ✓ | — | | [`array_replace_recursive()`](./builtins/array/array_replace_recursive.md) | `(array $array, array $replacements): mixed` | `mixed` | ✓ | — | | [`array_reverse()`](./builtins/array/array_reverse.md) | `(array $array): array` | `array` | ✓ | ✓ | | [`array_search()`](./builtins/array/array_search.md) | `(mixed $needle, array $haystack, bool $strict = false): mixed` | `mixed` | ✓ | ✓ | -| [`array_shift()`](./builtins/array/array_shift.md) | `(array $array): mixed` | `mixed` | ✓ | ✓ | +| [`array_shift()`](./builtins/array/array_shift.md) | `(array &$array): mixed` | `mixed` | ✓ | ✓ | | [`array_slice()`](./builtins/array/array_slice.md) | `(array $array, int $offset, int $length = null): array` | `array` | ✓ | ✓ | -| [`array_splice()`](./builtins/array/array_splice.md) | `(array $array, int $offset, int $length = null): array` | `array` | ✓ | ✓ | +| [`array_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_unshift()`](./builtins/array/array_unshift.md) | `(array &$array, ...$values): int` | `int` | ✓ | ✓ | | [`array_values()`](./builtins/array/array_values.md) | `(array $array): array` | `array` | ✓ | ✓ | -| [`array_walk()`](./builtins/array/array_walk.md) | `(array $array, callable $callback): void` | `void` | ✓ | ✓ | -| [`array_walk_recursive()`](./builtins/array/array_walk_recursive.md) | `(array $array, callable $callback): void` | `void` | ✓ | — | -| [`arsort()`](./builtins/array/arsort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | -| [`asort()`](./builtins/array/asort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`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` | ✓ | ✓ | +| [`krsort()`](./builtins/array/krsort.md) | `(array &$array): bool` | `bool` | ✓ | ✓ | +| [`ksort()`](./builtins/array/ksort.md) | `(array &$array): bool` | `bool` | ✓ | ✓ | +| [`natcasesort()`](./builtins/array/natcasesort.md) | `(array &$array): bool` | `bool` | ✓ | ✓ | +| [`natsort()`](./builtins/array/natsort.md) | `(array &$array): bool` | `bool` | ✓ | ✓ | | [`range()`](./builtins/array/range.md) | `(mixed $start, mixed $end): array` | `array` | ✓ | ✓ | -| [`rsort()`](./builtins/array/rsort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | -| [`shuffle()`](./builtins/array/shuffle.md) | `(array $array): bool` | `bool` | ✓ | ✓ | -| [`sort()`](./builtins/array/sort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | -| [`uasort()`](./builtins/array/uasort.md) | `(array $array, callable $callback): bool` | `bool` | ✓ | ✓ | -| [`uksort()`](./builtins/array/uksort.md) | `(array $array, callable $callback): bool` | `bool` | ✓ | ✓ | -| [`usort()`](./builtins/array/usort.md) | `(array $array, callable $callback): bool` | `bool` | ✓ | ✓ | +| [`rsort()`](./builtins/array/rsort.md) | `(array &$array): bool` | `bool` | ✓ | ✓ | +| [`shuffle()`](./builtins/array/shuffle.md) | `(array &$array): bool` | `bool` | ✓ | ✓ | +| [`sort()`](./builtins/array/sort.md) | `(array &$array): bool` | `bool` | ✓ | ✓ | +| [`uasort()`](./builtins/array/uasort.md) | `(array &$array, callable $callback): bool` | `bool` | ✓ | ✓ | +| [`uksort()`](./builtins/array/uksort.md) | `(array &$array, callable $callback): bool` | `bool` | ✓ | ✓ | +| [`usort()`](./builtins/array/usort.md) | `(array &$array, callable $callback): bool` | `bool` | ✓ | ✓ | | [`buffer_free()`](./builtins/buffer/buffer_free.md) | `(buffer $buffer): void` | `void` | ✓ | ✓ | | [`buffer_len()`](./builtins/buffer/buffer_len.md) | `(buffer $buffer): int` | `int` | ✓ | ✓ | | [`class_alias()`](./builtins/class/class_alias.md) | `(string $class, string $alias, bool $autoload = true): bool` | `bool` | ✓ | ✓ | @@ -114,9 +114,9 @@ sidebar: | [`time()`](./builtins/date/time.md) | `(): int` | `int` | ✓ | ✓ | | [`basename()`](./builtins/filesystem/basename.md) | `(string $path, string $suffix = ''): string` | `string` | ✓ | ✓ | | [`chdir()`](./builtins/filesystem/chdir.md) | `(string $directory): bool` | `bool` | ✓ | ✓ | -| [`chgrp()`](./builtins/filesystem/chgrp.md) | `(string $filename, string $group): bool` | `bool` | ✓ | ✓ | +| [`chgrp()`](./builtins/filesystem/chgrp.md) | `(string $filename, mixed $group): bool` | `bool` | ✓ | ✓ | | [`chmod()`](./builtins/filesystem/chmod.md) | `(string $filename, int $permissions): bool` | `bool` | ✓ | ✓ | -| [`chown()`](./builtins/filesystem/chown.md) | `(string $filename, string $user): bool` | `bool` | ✓ | ✓ | +| [`chown()`](./builtins/filesystem/chown.md) | `(string $filename, mixed $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` | ✓ | ✓ | @@ -127,10 +127,10 @@ sidebar: | [`filectime()`](./builtins/filesystem/filectime.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | | [`filegroup()`](./builtins/filesystem/filegroup.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | | [`fileinode()`](./builtins/filesystem/fileinode.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | -| [`filemtime()`](./builtins/filesystem/filemtime.md) | `(string $filename): int` | `int` | ✓ | ✓ | +| [`filemtime()`](./builtins/filesystem/filemtime.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | | [`fileowner()`](./builtins/filesystem/fileowner.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | | [`fileperms()`](./builtins/filesystem/fileperms.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | -| [`filesize()`](./builtins/filesystem/filesize.md) | `(string $filename): int` | `int` | ✓ | ✓ | +| [`filesize()`](./builtins/filesystem/filesize.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | | [`filetype()`](./builtins/filesystem/filetype.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | | [`fnmatch()`](./builtins/filesystem/fnmatch.md) | `(string $pattern, string $filename, int $flags = 0): bool` | `bool` | ✓ | ✓ | | [`getcwd()`](./builtins/filesystem/getcwd.md) | `(): string` | `string` | ✓ | ✓ | @@ -143,8 +143,8 @@ 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, string $group): bool` | `bool` | ✓ | ✓ | -| [`lchown()`](./builtins/filesystem/lchown.md) | `(string $filename, string $user): bool` | `bool` | ✓ | ✓ | +| [`lchgrp()`](./builtins/filesystem/lchgrp.md) | `(string $filename, mixed $group): bool` | `bool` | ✓ | ✓ | +| [`lchown()`](./builtins/filesystem/lchown.md) | `(string $filename, mixed $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` | ✓ | ✓ | @@ -162,7 +162,7 @@ sidebar: | [`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` | ✓ | ✓ | +| [`tempnam()`](./builtins/filesystem/tempnam.md) | `(string $directory, string $prefix): mixed` | `mixed` | ✓ | ✓ | | [`tmpfile()`](./builtins/filesystem/tmpfile.md) | `(): mixed` | `mixed` | ✓ | ✓ | | [`touch()`](./builtins/filesystem/touch.md) | `(string $filename, int $mtime = null, int $atime = null): bool` | `bool` | ✓ | ✓ | | [`umask()`](./builtins/filesystem/umask.md) | `(int $mask = null): int` | `int` | ✓ | ✓ | @@ -178,7 +178,7 @@ sidebar: | [`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` | ✓ | ✓ | +| [`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` | ✓ | ✓ | @@ -237,18 +237,18 @@ sidebar: | [`stream_is_local()`](./builtins/io/stream_is_local.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | | [`stream_isatty()`](./builtins/io/stream_isatty.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | | [`stream_resolve_include_path()`](./builtins/io/stream_resolve_include_path.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | -| [`stream_select()`](./builtins/io/stream_select.md) | `(array $read, array $write, array $except, int $seconds, int $microseconds = 0): int` | `int` | ✓ | ✓ | +| [`stream_select()`](./builtins/io/stream_select.md) | `(array &$read, array &$write, array &$except, int $seconds, int $microseconds = 0): int` | `int` | ✓ | ✓ | | [`stream_set_blocking()`](./builtins/io/stream_set_blocking.md) | `(resource $stream, bool $enable): bool` | `bool` | ✓ | ✓ | | [`stream_set_chunk_size()`](./builtins/io/stream_set_chunk_size.md) | `(resource $stream, int $size): int` | `int` | ✓ | ✓ | | [`stream_set_read_buffer()`](./builtins/io/stream_set_read_buffer.md) | `(resource $stream, int $size): int` | `int` | ✓ | ✓ | | [`stream_set_timeout()`](./builtins/io/stream_set_timeout.md) | `(resource $stream, int $seconds, int $microseconds = 0): bool` | `bool` | ✓ | ✓ | | [`stream_set_write_buffer()`](./builtins/io/stream_set_write_buffer.md) | `(resource $stream, int $size): int` | `int` | ✓ | ✓ | -| [`stream_socket_accept()`](./builtins/io/stream_socket_accept.md) | `(resource $socket, float $timeout = null, string $peer_name = null): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_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_enable_crypto()`](./builtins/io/stream_socket_enable_crypto.md) | `(resource $stream, bool $enable, int $crypto_method = null, resource $session_stream = null): mixed` | `mixed` | ✓ | ✓ | | [`stream_socket_get_name()`](./builtins/io/stream_socket_get_name.md) | `(resource $socket, bool $remote): mixed` | `mixed` | ✓ | ✓ | | [`stream_socket_pair()`](./builtins/io/stream_socket_pair.md) | `(int $domain, int $type, int $protocol): mixed` | `mixed` | ✓ | ✓ | -| [`stream_socket_recvfrom()`](./builtins/io/stream_socket_recvfrom.md) | `(resource $socket, int $length, int $flags = 0, string $address = ''): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_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` | ✓ | ✓ | @@ -291,6 +291,7 @@ sidebar: | [`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 = 0): float` | `float` | ✓ | ✓ | | [`sin()`](./builtins/math/sin.md) | `(float $num): float` | `float` | ✓ | ✓ | @@ -337,13 +338,17 @@ sidebar: | [`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` | ✓ | ✓ | +| [`proc_close()`](./builtins/process/proc_close.md) | `(resource $process): int` | `int` | ✓ | ✓ | +| [`proc_get_status()`](./builtins/process/proc_get_status.md) | `(resource $process): array|false` | `array|false` | ✓ | ✓ | +| [`proc_open()`](./builtins/process/proc_open.md) | `(array|string $command, array $descriptor_spec, array &$pipes, ?string $cwd = null, ?array $env_vars = null, ?array $options = null): resource|false` | `resource|false` | ✓ | ✓ | +| [`proc_terminate()`](./builtins/process/proc_terminate.md) | `(resource $process, int $signal = 15): bool` | `bool` | ✓ | ✓ | | [`readline()`](./builtins/process/readline.md) | `(string $prompt = null): mixed` | `mixed` | ✓ | ✓ | | [`shell_exec()`](./builtins/process/shell_exec.md) | `(string $command): string` | `string` | ✓ | ✓ | | [`sleep()`](./builtins/process/sleep.md) | `(int $seconds): int` | `int` | ✓ | ✓ | | [`system()`](./builtins/process/system.md) | `(string $command): string` | `string` | ✓ | ✓ | | [`usleep()`](./builtins/process/usleep.md) | `(int $microseconds): void` | `void` | ✓ | ✓ | | [`mb_ereg_match()`](./builtins/regex/mb_ereg_match.md) | `(string $pattern, string $subject, string $options = null): bool` | `bool` | ✓ | ✓ | -| [`preg_match()`](./builtins/regex/preg_match.md) | `(string $pattern, string $subject, array $matches = []): int` | `int` | ✓ | ✓ | +| [`preg_match()`](./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` | ✓ | ✓ | @@ -360,8 +365,8 @@ sidebar: | [`spl_classes()`](./builtins/spl/spl_classes.md) | `(): array` | `array` | ✓ | ✓ | | [`spl_object_hash()`](./builtins/spl/spl_object_hash.md) | `(object $object): string` | `string` | ✓ | ✓ | | [`spl_object_id()`](./builtins/spl/spl_object_id.md) | `(object $object): int` | `int` | ✓ | ✓ | -| [`fsockopen()`](./builtins/streams/fsockopen.md) | `(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed` | `mixed` | ✓ | ✓ | -| [`pfsockopen()`](./builtins/streams/pfsockopen.md) | `(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed` | `mixed` | ✓ | ✓ | +| [`fsockopen()`](./builtins/streams/fsockopen.md) | `(string $hostname, int $port, int &$error_code = null, string &$error_message = null, float $timeout = null): mixed` | `mixed` | ✓ | ✓ | +| [`pfsockopen()`](./builtins/streams/pfsockopen.md) | `(string $hostname, int $port, int &$error_code = null, string &$error_message = null, float $timeout = null): mixed` | `mixed` | ✓ | ✓ | | [`stream_bucket_append()`](./builtins/streams/stream_bucket_append.md) | `(mixed $brigade, mixed $bucket): void` | `void` | ✓ | ✓ | | [`stream_bucket_prepend()`](./builtins/streams/stream_bucket_prepend.md) | `(mixed $brigade, mixed $bucket): void` | `void` | ✓ | ✓ | | [`stream_filter_append()`](./builtins/streams/stream_filter_append.md) | `(resource $stream, string $filtername, int $read_write = 3, mixed $params = null): mixed` | `mixed` | ✓ | ✓ | @@ -373,6 +378,8 @@ sidebar: | [`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` | ✓ | ✓ | +| [`escapeshellarg()`](./builtins/string/escapeshellarg.md) | `(string $arg): string` | `string` | ✓ | ✓ | +| [`escapeshellcmd()`](./builtins/string/escapeshellcmd.md) | `(string $command): string` | `string` | ✓ | ✓ | | [`explode()`](./builtins/string/explode.md) | `(string $separator, string $string, int $limit = PHP_INT_MAX): array` | `array` | ✓ | ✓ | | [`grapheme_strrev()`](./builtins/string/grapheme_strrev.md) | `(string $string): mixed` | `mixed` | ✓ | ✓ | | [`gzcompress()`](./builtins/string/gzcompress.md) | `(string $data, int $level = -1): string` | `string` | ✓ | ✓ | @@ -398,7 +405,7 @@ sidebar: | [`lcfirst()`](./builtins/string/lcfirst.md) | `(string $string): string` | `string` | ✓ | ✓ | | [`long2ip()`](./builtins/string/long2ip.md) | `(int $ip): string` | `string` | ✓ | ✓ | | [`ltrim()`](./builtins/string/ltrim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | -| [`mb_strlen()`](./builtins/string/mb_strlen.md) | `(string $string, string $encoding = null): int` | `int` | ✓ | ✓ | +| [`mb_strlen()`](./builtins/string/mb_strlen.md) | `(string $string, mixed $encoding = null): int` | `int` | ✓ | ✓ | | [`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` | ✓ | ✓ | @@ -464,5 +471,5 @@ sidebar: | [`is_resource()`](./builtins/type/is_resource.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | | [`is_scalar()`](./builtins/type/is_scalar.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | | [`is_string()`](./builtins/type/is_string.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | -| [`settype()`](./builtins/type/settype.md) | `(mixed $var, string $type): bool` | `bool` | ✓ | ✓ | +| [`settype()`](./builtins/type/settype.md) | `(mixed &$var, string $type): bool` | `bool` | ✓ | ✓ | | [`strval()`](./builtins/type/strval.md) | `(mixed $value): string` | `string` | ✓ | ✓ | diff --git a/docs/php/builtins/array.md b/docs/php/builtins/array.md index 3cee753978..b3f618a275 100644 --- a/docs/php/builtins/array.md +++ b/docs/php/builtins/array.md @@ -33,42 +33,42 @@ sidebar: | [`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_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_pop()`](./array/array_pop.md) | `(array &$array): mixed` | `mixed` | ✓ | ✓ | | [`array_product()`](./array/array_product.md) | `(array $array): int` | `int` | ✓ | ✓ | -| [`array_push()`](./array/array_push.md) | `(array $array, ...$values): void` | `void` | ✓ | ✓ | +| [`array_push()`](./array/array_push.md) | `(array &$array, ...$values): void` | `void` | ✓ | ✓ | | [`array_rand()`](./array/array_rand.md) | `(array $array): int` | `int` | ✓ | ✓ | | [`array_reduce()`](./array/array_reduce.md) | `(array $array, callable $callback, mixed $initial = null): int` | `int` | ✓ | ✓ | | [`array_replace()`](./array/array_replace.md) | `(array $array, array $replacements): mixed` | `mixed` | ✓ | — | | [`array_replace_recursive()`](./array/array_replace_recursive.md) | `(array $array, array $replacements): mixed` | `mixed` | ✓ | — | | [`array_reverse()`](./array/array_reverse.md) | `(array $array): array` | `array` | ✓ | ✓ | | [`array_search()`](./array/array_search.md) | `(mixed $needle, array $haystack, bool $strict = false): mixed` | `mixed` | ✓ | ✓ | -| [`array_shift()`](./array/array_shift.md) | `(array $array): mixed` | `mixed` | ✓ | ✓ | +| [`array_shift()`](./array/array_shift.md) | `(array &$array): mixed` | `mixed` | ✓ | ✓ | | [`array_slice()`](./array/array_slice.md) | `(array $array, int $offset, int $length = null): array` | `array` | ✓ | ✓ | -| [`array_splice()`](./array/array_splice.md) | `(array $array, int $offset, int $length = null): array` | `array` | ✓ | ✓ | +| [`array_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_unshift()`](./array/array_unshift.md) | `(array &$array, ...$values): int` | `int` | ✓ | ✓ | | [`array_values()`](./array/array_values.md) | `(array $array): array` | `array` | ✓ | ✓ | -| [`array_walk()`](./array/array_walk.md) | `(array $array, callable $callback): void` | `void` | ✓ | ✓ | -| [`array_walk_recursive()`](./array/array_walk_recursive.md) | `(array $array, callable $callback): void` | `void` | ✓ | — | -| [`arsort()`](./array/arsort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | -| [`asort()`](./array/asort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`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` | ✓ | ✓ | +| [`krsort()`](./array/krsort.md) | `(array &$array): bool` | `bool` | ✓ | ✓ | +| [`ksort()`](./array/ksort.md) | `(array &$array): bool` | `bool` | ✓ | ✓ | +| [`natcasesort()`](./array/natcasesort.md) | `(array &$array): bool` | `bool` | ✓ | ✓ | +| [`natsort()`](./array/natsort.md) | `(array &$array): bool` | `bool` | ✓ | ✓ | | [`range()`](./array/range.md) | `(mixed $start, mixed $end): array` | `array` | ✓ | ✓ | -| [`rsort()`](./array/rsort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | -| [`shuffle()`](./array/shuffle.md) | `(array $array): bool` | `bool` | ✓ | ✓ | -| [`sort()`](./array/sort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | -| [`uasort()`](./array/uasort.md) | `(array $array, callable $callback): bool` | `bool` | ✓ | ✓ | -| [`uksort()`](./array/uksort.md) | `(array $array, callable $callback): bool` | `bool` | ✓ | ✓ | -| [`usort()`](./array/usort.md) | `(array $array, callable $callback): bool` | `bool` | ✓ | ✓ | +| [`rsort()`](./array/rsort.md) | `(array &$array): bool` | `bool` | ✓ | ✓ | +| [`shuffle()`](./array/shuffle.md) | `(array &$array): bool` | `bool` | ✓ | ✓ | +| [`sort()`](./array/sort.md) | `(array &$array): bool` | `bool` | ✓ | ✓ | +| [`uasort()`](./array/uasort.md) | `(array &$array, callable $callback): bool` | `bool` | ✓ | ✓ | +| [`uksort()`](./array/uksort.md) | `(array &$array, callable $callback): bool` | `bool` | ✓ | ✓ | +| [`usort()`](./array/usort.md) | `(array &$array, callable $callback): bool` | `bool` | ✓ | ✓ | diff --git a/docs/php/builtins/array/array_multisort.md b/docs/php/builtins/array/array_multisort.md index 57e77e53ef..b2898cdd6e 100644 --- a/docs/php/builtins/array/array_multisort.md +++ b/docs/php/builtins/array/array_multisort.md @@ -8,7 +8,7 @@ sidebar: ## array_multisort() ```php -function array_multisort(array $array1, int $array2): bool +function array_multisort(array &$array1, int &$array2): bool ``` Sorts multiple arrays or multi-dimensional arrays. diff --git a/docs/php/builtins/array/array_pop.md b/docs/php/builtins/array/array_pop.md index 8c978dda86..bdc27185ea 100644 --- a/docs/php/builtins/array/array_pop.md +++ b/docs/php/builtins/array/array_pop.md @@ -8,7 +8,7 @@ sidebar: ## array_pop() ```php -function array_pop(array $array): mixed +function array_pop(array &$array): mixed ``` Pops the element off the end of array. diff --git a/docs/php/builtins/array/array_push.md b/docs/php/builtins/array/array_push.md index ff36fccdf4..3d41d8c608 100644 --- a/docs/php/builtins/array/array_push.md +++ b/docs/php/builtins/array/array_push.md @@ -8,7 +8,7 @@ sidebar: ## array_push() ```php -function array_push(array $array, ...$values): void +function array_push(array &$array, ...$values): void ``` Pushes one or more elements onto the end of array. diff --git a/docs/php/builtins/array/array_shift.md b/docs/php/builtins/array/array_shift.md index 4e2f14ad45..2ef6a4db36 100644 --- a/docs/php/builtins/array/array_shift.md +++ b/docs/php/builtins/array/array_shift.md @@ -8,7 +8,7 @@ sidebar: ## array_shift() ```php -function array_shift(array $array): mixed +function array_shift(array &$array): mixed ``` Shifts an element off the beginning of array. diff --git a/docs/php/builtins/array/array_splice.md b/docs/php/builtins/array/array_splice.md index 4cc6355c91..1d32543099 100644 --- a/docs/php/builtins/array/array_splice.md +++ b/docs/php/builtins/array/array_splice.md @@ -8,7 +8,7 @@ sidebar: ## array_splice() ```php -function array_splice(array $array, int $offset, int $length = null): array +function array_splice(array &$array, int $offset, int $length = null): array ``` Removes a portion of the array and replaces it with something else. diff --git a/docs/php/builtins/array/array_unshift.md b/docs/php/builtins/array/array_unshift.md index 5c1cec2686..82d5a4baa8 100644 --- a/docs/php/builtins/array/array_unshift.md +++ b/docs/php/builtins/array/array_unshift.md @@ -8,7 +8,7 @@ sidebar: ## array_unshift() ```php -function array_unshift(array $array, ...$values): int +function array_unshift(array &$array, ...$values): int ``` Prepends one or more elements to the beginning of an array. diff --git a/docs/php/builtins/array/array_walk.md b/docs/php/builtins/array/array_walk.md index 522c630d5c..184d5b874a 100644 --- a/docs/php/builtins/array/array_walk.md +++ b/docs/php/builtins/array/array_walk.md @@ -8,7 +8,7 @@ sidebar: ## array_walk() ```php -function array_walk(array $array, callable $callback): void +function array_walk(array &$array, callable $callback): void ``` Applies a user function to every member of an array. diff --git a/docs/php/builtins/array/array_walk_recursive.md b/docs/php/builtins/array/array_walk_recursive.md index a2b37de046..94f788641b 100644 --- a/docs/php/builtins/array/array_walk_recursive.md +++ b/docs/php/builtins/array/array_walk_recursive.md @@ -8,7 +8,7 @@ sidebar: ## array_walk_recursive() ```php -function array_walk_recursive(array $array, callable $callback): void +function array_walk_recursive(array &$array, callable $callback): void ``` Applies a user function recursively to every member of an array. diff --git a/docs/php/builtins/array/arsort.md b/docs/php/builtins/array/arsort.md index 172ce60903..b07b7da480 100644 --- a/docs/php/builtins/array/arsort.md +++ b/docs/php/builtins/array/arsort.md @@ -8,7 +8,7 @@ sidebar: ## arsort() ```php -function arsort(array $array): bool +function arsort(array &$array): bool ``` Sorts an array in descending order and maintains index association. diff --git a/docs/php/builtins/array/asort.md b/docs/php/builtins/array/asort.md index 209d5eaf0f..0c1a4afd6d 100644 --- a/docs/php/builtins/array/asort.md +++ b/docs/php/builtins/array/asort.md @@ -8,7 +8,7 @@ sidebar: ## asort() ```php -function asort(array $array): bool +function asort(array &$array): bool ``` Sorts an array and maintains index association. diff --git a/docs/php/builtins/array/krsort.md b/docs/php/builtins/array/krsort.md index 119620599d..aae2241273 100644 --- a/docs/php/builtins/array/krsort.md +++ b/docs/php/builtins/array/krsort.md @@ -8,7 +8,7 @@ sidebar: ## krsort() ```php -function krsort(array $array): bool +function krsort(array &$array): bool ``` Sorts an array by key in descending order. diff --git a/docs/php/builtins/array/ksort.md b/docs/php/builtins/array/ksort.md index dea6a01929..15acbd0446 100644 --- a/docs/php/builtins/array/ksort.md +++ b/docs/php/builtins/array/ksort.md @@ -8,7 +8,7 @@ sidebar: ## ksort() ```php -function ksort(array $array): bool +function ksort(array &$array): bool ``` Sorts an array by key in ascending order. diff --git a/docs/php/builtins/array/natcasesort.md b/docs/php/builtins/array/natcasesort.md index b679f6c411..8f6645cbd9 100644 --- a/docs/php/builtins/array/natcasesort.md +++ b/docs/php/builtins/array/natcasesort.md @@ -8,7 +8,7 @@ sidebar: ## natcasesort() ```php -function natcasesort(array $array): bool +function natcasesort(array &$array): bool ``` Sorts an array using a case-insensitive natural order algorithm. diff --git a/docs/php/builtins/array/natsort.md b/docs/php/builtins/array/natsort.md index 857c744247..8266ca1e54 100644 --- a/docs/php/builtins/array/natsort.md +++ b/docs/php/builtins/array/natsort.md @@ -8,7 +8,7 @@ sidebar: ## natsort() ```php -function natsort(array $array): bool +function natsort(array &$array): bool ``` Sorts an array using a natural order algorithm. diff --git a/docs/php/builtins/array/rsort.md b/docs/php/builtins/array/rsort.md index 27506824b2..851d35ce46 100644 --- a/docs/php/builtins/array/rsort.md +++ b/docs/php/builtins/array/rsort.md @@ -8,7 +8,7 @@ sidebar: ## rsort() ```php -function rsort(array $array): bool +function rsort(array &$array): bool ``` Sorts an array in descending order. diff --git a/docs/php/builtins/array/shuffle.md b/docs/php/builtins/array/shuffle.md index 78cd6db0fd..bd6a0e8c89 100644 --- a/docs/php/builtins/array/shuffle.md +++ b/docs/php/builtins/array/shuffle.md @@ -8,7 +8,7 @@ sidebar: ## shuffle() ```php -function shuffle(array $array): bool +function shuffle(array &$array): bool ``` Shuffles an array into random order. diff --git a/docs/php/builtins/array/sort.md b/docs/php/builtins/array/sort.md index 1f5b022a0c..4523ed2144 100644 --- a/docs/php/builtins/array/sort.md +++ b/docs/php/builtins/array/sort.md @@ -8,7 +8,7 @@ sidebar: ## sort() ```php -function sort(array $array): bool +function sort(array &$array): bool ``` Sorts an array in ascending order. diff --git a/docs/php/builtins/array/uasort.md b/docs/php/builtins/array/uasort.md index a9cc2a15d5..5b86cdb9a0 100644 --- a/docs/php/builtins/array/uasort.md +++ b/docs/php/builtins/array/uasort.md @@ -8,7 +8,7 @@ sidebar: ## uasort() ```php -function uasort(array $array, callable $callback): bool +function uasort(array &$array, callable $callback): bool ``` Sorts an array with a user-defined comparison function and maintains index association. diff --git a/docs/php/builtins/array/uksort.md b/docs/php/builtins/array/uksort.md index be0a93da3e..711ec032d0 100644 --- a/docs/php/builtins/array/uksort.md +++ b/docs/php/builtins/array/uksort.md @@ -8,7 +8,7 @@ sidebar: ## uksort() ```php -function uksort(array $array, callable $callback): bool +function uksort(array &$array, callable $callback): bool ``` Sorts an array by keys using a user-defined comparison function. diff --git a/docs/php/builtins/array/usort.md b/docs/php/builtins/array/usort.md index d227ad0360..162e582836 100644 --- a/docs/php/builtins/array/usort.md +++ b/docs/php/builtins/array/usort.md @@ -8,7 +8,7 @@ sidebar: ## usort() ```php -function usort(array $array, callable $callback): bool +function usort(array &$array, callable $callback): bool ``` Sorts an array by values using a user-defined comparison function. diff --git a/docs/php/builtins/filesystem.md b/docs/php/builtins/filesystem.md index d8c9af2cf3..e14b42d6ea 100644 --- a/docs/php/builtins/filesystem.md +++ b/docs/php/builtins/filesystem.md @@ -11,9 +11,9 @@ sidebar: |---|---|---|:-:|:-:| | [`basename()`](./filesystem/basename.md) | `(string $path, string $suffix = ''): string` | `string` | ✓ | ✓ | | [`chdir()`](./filesystem/chdir.md) | `(string $directory): bool` | `bool` | ✓ | ✓ | -| [`chgrp()`](./filesystem/chgrp.md) | `(string $filename, string $group): bool` | `bool` | ✓ | ✓ | +| [`chgrp()`](./filesystem/chgrp.md) | `(string $filename, mixed $group): bool` | `bool` | ✓ | ✓ | | [`chmod()`](./filesystem/chmod.md) | `(string $filename, int $permissions): bool` | `bool` | ✓ | ✓ | -| [`chown()`](./filesystem/chown.md) | `(string $filename, string $user): bool` | `bool` | ✓ | ✓ | +| [`chown()`](./filesystem/chown.md) | `(string $filename, mixed $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` | ✓ | ✓ | @@ -24,10 +24,10 @@ sidebar: | [`filectime()`](./filesystem/filectime.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | | [`filegroup()`](./filesystem/filegroup.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | | [`fileinode()`](./filesystem/fileinode.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | -| [`filemtime()`](./filesystem/filemtime.md) | `(string $filename): int` | `int` | ✓ | ✓ | +| [`filemtime()`](./filesystem/filemtime.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | | [`fileowner()`](./filesystem/fileowner.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | | [`fileperms()`](./filesystem/fileperms.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | -| [`filesize()`](./filesystem/filesize.md) | `(string $filename): int` | `int` | ✓ | ✓ | +| [`filesize()`](./filesystem/filesize.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | | [`filetype()`](./filesystem/filetype.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | | [`fnmatch()`](./filesystem/fnmatch.md) | `(string $pattern, string $filename, int $flags = 0): bool` | `bool` | ✓ | ✓ | | [`getcwd()`](./filesystem/getcwd.md) | `(): string` | `string` | ✓ | ✓ | @@ -40,8 +40,8 @@ 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, string $group): bool` | `bool` | ✓ | ✓ | -| [`lchown()`](./filesystem/lchown.md) | `(string $filename, string $user): bool` | `bool` | ✓ | ✓ | +| [`lchgrp()`](./filesystem/lchgrp.md) | `(string $filename, mixed $group): bool` | `bool` | ✓ | ✓ | +| [`lchown()`](./filesystem/lchown.md) | `(string $filename, mixed $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` | ✓ | ✓ | @@ -59,7 +59,7 @@ sidebar: | [`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` | ✓ | ✓ | +| [`tempnam()`](./filesystem/tempnam.md) | `(string $directory, string $prefix): mixed` | `mixed` | ✓ | ✓ | | [`tmpfile()`](./filesystem/tmpfile.md) | `(): mixed` | `mixed` | ✓ | ✓ | | [`touch()`](./filesystem/touch.md) | `(string $filename, int $mtime = null, int $atime = null): bool` | `bool` | ✓ | ✓ | | [`umask()`](./filesystem/umask.md) | `(int $mask = null): int` | `int` | ✓ | ✓ | diff --git a/docs/php/builtins/filesystem/chgrp.md b/docs/php/builtins/filesystem/chgrp.md index deb4029355..c9ad0fa495 100644 --- a/docs/php/builtins/filesystem/chgrp.md +++ b/docs/php/builtins/filesystem/chgrp.md @@ -8,14 +8,14 @@ sidebar: ## chgrp() ```php -function chgrp(string $filename, string $group): bool +function chgrp(string $filename, mixed $group): bool ``` Changes file group. **Parameters**: - `$filename` (`string`) -- `$group` (`string`) +- `$group` (`mixed`) **Returns**: `bool` diff --git a/docs/php/builtins/filesystem/chown.md b/docs/php/builtins/filesystem/chown.md index 8b57c91612..d1c73ae295 100644 --- a/docs/php/builtins/filesystem/chown.md +++ b/docs/php/builtins/filesystem/chown.md @@ -8,14 +8,14 @@ sidebar: ## chown() ```php -function chown(string $filename, string $user): bool +function chown(string $filename, mixed $user): bool ``` Changes file owner. **Parameters**: - `$filename` (`string`) -- `$user` (`string`) +- `$user` (`mixed`) **Returns**: `bool` diff --git a/docs/php/builtins/filesystem/filemtime.md b/docs/php/builtins/filesystem/filemtime.md index d450fe72e5..60356743e7 100644 --- a/docs/php/builtins/filesystem/filemtime.md +++ b/docs/php/builtins/filesystem/filemtime.md @@ -8,7 +8,7 @@ sidebar: ## filemtime() ```php -function filemtime(string $filename): int +function filemtime(string $filename): mixed ``` Gets file modification time. @@ -16,7 +16,7 @@ Gets file modification time. **Parameters**: - `$filename` (`string`) -**Returns**: `int` +**Returns**: `mixed` ## Availability diff --git a/docs/php/builtins/filesystem/filesize.md b/docs/php/builtins/filesystem/filesize.md index 8e533bf071..a33172f0b5 100644 --- a/docs/php/builtins/filesystem/filesize.md +++ b/docs/php/builtins/filesystem/filesize.md @@ -8,7 +8,7 @@ sidebar: ## filesize() ```php -function filesize(string $filename): int +function filesize(string $filename): mixed ``` Gets file size. @@ -16,7 +16,7 @@ Gets file size. **Parameters**: - `$filename` (`string`) -**Returns**: `int` +**Returns**: `mixed` ## Availability diff --git a/docs/php/builtins/filesystem/lchgrp.md b/docs/php/builtins/filesystem/lchgrp.md index da391127bb..0304dd339f 100644 --- a/docs/php/builtins/filesystem/lchgrp.md +++ b/docs/php/builtins/filesystem/lchgrp.md @@ -8,14 +8,14 @@ sidebar: ## lchgrp() ```php -function lchgrp(string $filename, string $group): bool +function lchgrp(string $filename, mixed $group): bool ``` Changes group ownership of a symlink. **Parameters**: - `$filename` (`string`) -- `$group` (`string`) +- `$group` (`mixed`) **Returns**: `bool` diff --git a/docs/php/builtins/filesystem/lchown.md b/docs/php/builtins/filesystem/lchown.md index bbd89e1877..73c2820bda 100644 --- a/docs/php/builtins/filesystem/lchown.md +++ b/docs/php/builtins/filesystem/lchown.md @@ -8,14 +8,14 @@ sidebar: ## lchown() ```php -function lchown(string $filename, string $user): bool +function lchown(string $filename, mixed $user): bool ``` Changes user ownership of a symlink. **Parameters**: - `$filename` (`string`) -- `$user` (`string`) +- `$user` (`mixed`) **Returns**: `bool` diff --git a/docs/php/builtins/filesystem/tempnam.md b/docs/php/builtins/filesystem/tempnam.md index 3a6f82fa74..fa401ca6ce 100644 --- a/docs/php/builtins/filesystem/tempnam.md +++ b/docs/php/builtins/filesystem/tempnam.md @@ -8,7 +8,7 @@ sidebar: ## tempnam() ```php -function tempnam(string $directory, string $prefix): string +function tempnam(string $directory, string $prefix): mixed ``` Creates a file with a unique filename. @@ -17,7 +17,7 @@ Creates a file with a unique filename. - `$directory` (`string`) - `$prefix` (`string`) -**Returns**: `string` +**Returns**: `mixed` ## Availability diff --git a/docs/php/builtins/io.md b/docs/php/builtins/io.md index 473850ffb6..fc359935f5 100644 --- a/docs/php/builtins/io.md +++ b/docs/php/builtins/io.md @@ -20,7 +20,7 @@ sidebar: | [`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` | ✓ | ✓ | +| [`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` | ✓ | ✓ | @@ -79,18 +79,18 @@ sidebar: | [`stream_is_local()`](./io/stream_is_local.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | | [`stream_isatty()`](./io/stream_isatty.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | | [`stream_resolve_include_path()`](./io/stream_resolve_include_path.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | -| [`stream_select()`](./io/stream_select.md) | `(array $read, array $write, array $except, int $seconds, int $microseconds = 0): int` | `int` | ✓ | ✓ | +| [`stream_select()`](./io/stream_select.md) | `(array &$read, array &$write, array &$except, int $seconds, int $microseconds = 0): int` | `int` | ✓ | ✓ | | [`stream_set_blocking()`](./io/stream_set_blocking.md) | `(resource $stream, bool $enable): bool` | `bool` | ✓ | ✓ | | [`stream_set_chunk_size()`](./io/stream_set_chunk_size.md) | `(resource $stream, int $size): int` | `int` | ✓ | ✓ | | [`stream_set_read_buffer()`](./io/stream_set_read_buffer.md) | `(resource $stream, int $size): int` | `int` | ✓ | ✓ | | [`stream_set_timeout()`](./io/stream_set_timeout.md) | `(resource $stream, int $seconds, int $microseconds = 0): bool` | `bool` | ✓ | ✓ | | [`stream_set_write_buffer()`](./io/stream_set_write_buffer.md) | `(resource $stream, int $size): int` | `int` | ✓ | ✓ | -| [`stream_socket_accept()`](./io/stream_socket_accept.md) | `(resource $socket, float $timeout = null, string $peer_name = null): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_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_enable_crypto()`](./io/stream_socket_enable_crypto.md) | `(resource $stream, bool $enable, int $crypto_method = null, resource $session_stream = null): mixed` | `mixed` | ✓ | ✓ | | [`stream_socket_get_name()`](./io/stream_socket_get_name.md) | `(resource $socket, bool $remote): mixed` | `mixed` | ✓ | ✓ | | [`stream_socket_pair()`](./io/stream_socket_pair.md) | `(int $domain, int $type, int $protocol): mixed` | `mixed` | ✓ | ✓ | -| [`stream_socket_recvfrom()`](./io/stream_socket_recvfrom.md) | `(resource $socket, int $length, int $flags = 0, string $address = ''): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_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` | ✓ | ✓ | diff --git a/docs/php/builtins/io/flock.md b/docs/php/builtins/io/flock.md index c7b78263d1..fa605d4163 100644 --- a/docs/php/builtins/io/flock.md +++ b/docs/php/builtins/io/flock.md @@ -8,7 +8,7 @@ sidebar: ## flock() ```php -function flock(resource $stream, int $operation, bool $would_block = null): bool +function flock(resource $stream, int $operation, bool &$would_block = null): bool ``` Portable advisory file locking. diff --git a/docs/php/builtins/io/stream_select.md b/docs/php/builtins/io/stream_select.md index 4870e48170..d0d6d8fbd5 100644 --- a/docs/php/builtins/io/stream_select.md +++ b/docs/php/builtins/io/stream_select.md @@ -8,7 +8,7 @@ sidebar: ## stream_select() ```php -function stream_select(array $read, array $write, array $except, int $seconds, int $microseconds = 0): int +function stream_select(array &$read, array &$write, array &$except, int $seconds, int $microseconds = 0): int ``` Runs the equivalent of the select() system call on the given arrays of streams. diff --git a/docs/php/builtins/io/stream_socket_accept.md b/docs/php/builtins/io/stream_socket_accept.md index c0ea571d00..8c02fd2763 100644 --- a/docs/php/builtins/io/stream_socket_accept.md +++ b/docs/php/builtins/io/stream_socket_accept.md @@ -8,7 +8,7 @@ sidebar: ## stream_socket_accept() ```php -function stream_socket_accept(resource $socket, float $timeout = null, string $peer_name = null): mixed +function stream_socket_accept(resource $socket, float $timeout = null, string &$peer_name = null): mixed ``` Accept a connection on a socket created by stream_socket_server(). diff --git a/docs/php/builtins/io/stream_socket_enable_crypto.md b/docs/php/builtins/io/stream_socket_enable_crypto.md index 2011629b29..4540ecf35d 100644 --- a/docs/php/builtins/io/stream_socket_enable_crypto.md +++ b/docs/php/builtins/io/stream_socket_enable_crypto.md @@ -8,7 +8,7 @@ sidebar: ## stream_socket_enable_crypto() ```php -function stream_socket_enable_crypto(resource $stream, bool $enable, int $crypto_method = null, resource $session_stream = null): bool +function stream_socket_enable_crypto(resource $stream, bool $enable, int $crypto_method = null, resource $session_stream = null): mixed ``` Turns encryption on/off on an already connected socket. @@ -19,7 +19,7 @@ Turns encryption on/off on an already connected socket. - `$crypto_method` (`int`), default `null`, optional - `$session_stream` (`resource`), default `null`, optional -**Returns**: `bool` +**Returns**: `mixed` ## Availability diff --git a/docs/php/builtins/io/stream_socket_recvfrom.md b/docs/php/builtins/io/stream_socket_recvfrom.md index 0ea8687d35..7428b8df65 100644 --- a/docs/php/builtins/io/stream_socket_recvfrom.md +++ b/docs/php/builtins/io/stream_socket_recvfrom.md @@ -8,7 +8,7 @@ sidebar: ## stream_socket_recvfrom() ```php -function stream_socket_recvfrom(resource $socket, int $length, int $flags = 0, string $address = ''): mixed +function stream_socket_recvfrom(resource $socket, int $length, int $flags = 0, string &$address = ''): mixed ``` Receives data from a socket, connected or not. diff --git a/docs/php/builtins/math.md b/docs/php/builtins/math.md index 39b0a972a3..2c7c7a7d78 100644 --- a/docs/php/builtins/math.md +++ b/docs/php/builtins/math.md @@ -38,6 +38,7 @@ sidebar: | [`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 = 0): float` | `float` | ✓ | ✓ | | [`sin()`](./math/sin.md) | `(float $num): float` | `float` | ✓ | ✓ | diff --git a/docs/php/builtins/math/random_bytes.md b/docs/php/builtins/math/random_bytes.md new file mode 100644 index 0000000000..ae0087d934 --- /dev/null +++ b/docs/php/builtins/math/random_bytes.md @@ -0,0 +1,36 @@ +--- +title: "random_bytes()" +description: "Get a cryptographically secure random string of the given length." +sidebar: + order: 283 +--- + +## random_bytes() + +```php +function random_bytes(int $length): string +``` + +Get a cryptographically secure random string of the given length. + +**Parameters**: +- `$length` (`int`) + +**Returns**: `string` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/random_bytes.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/random_bytes.rs)). + +_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 567ee28753..dab79b2025 100644 --- a/docs/php/builtins/math/random_int.md +++ b/docs/php/builtins/math/random_int.md @@ -2,7 +2,7 @@ title: "random_int()" description: "Get a cryptographically secure, uniformly selected integer." sidebar: - order: 283 + order: 284 --- ## random_int() diff --git a/docs/php/builtins/math/round.md b/docs/php/builtins/math/round.md index c0240ef99d..8820dd05c9 100644 --- a/docs/php/builtins/math/round.md +++ b/docs/php/builtins/math/round.md @@ -2,7 +2,7 @@ title: "round()" description: "Rounds a float." sidebar: - order: 284 + order: 285 --- ## round() diff --git a/docs/php/builtins/math/sin.md b/docs/php/builtins/math/sin.md index babbb6ed17..1bde01822a 100644 --- a/docs/php/builtins/math/sin.md +++ b/docs/php/builtins/math/sin.md @@ -2,7 +2,7 @@ title: "sin()" description: "Returns the sine of a number (radians)." sidebar: - order: 285 + order: 286 --- ## sin() diff --git a/docs/php/builtins/math/sinh.md b/docs/php/builtins/math/sinh.md index d75da63b73..24d1ceb632 100644 --- a/docs/php/builtins/math/sinh.md +++ b/docs/php/builtins/math/sinh.md @@ -2,7 +2,7 @@ title: "sinh()" description: "Returns the hyperbolic sine of a number." sidebar: - order: 286 + order: 287 --- ## sinh() diff --git a/docs/php/builtins/math/sqrt.md b/docs/php/builtins/math/sqrt.md index 90678de8e1..4947872f96 100644 --- a/docs/php/builtins/math/sqrt.md +++ b/docs/php/builtins/math/sqrt.md @@ -2,7 +2,7 @@ title: "sqrt()" description: "Returns the square root of a number." sidebar: - order: 287 + order: 288 --- ## sqrt() diff --git a/docs/php/builtins/math/tan.md b/docs/php/builtins/math/tan.md index 1a01e9bb53..bf2a15a6cf 100644 --- a/docs/php/builtins/math/tan.md +++ b/docs/php/builtins/math/tan.md @@ -2,7 +2,7 @@ title: "tan()" description: "Returns the tangent of a number (radians)." sidebar: - order: 288 + order: 289 --- ## tan() diff --git a/docs/php/builtins/math/tanh.md b/docs/php/builtins/math/tanh.md index 1d5474d4ac..3d4e88f476 100644 --- a/docs/php/builtins/math/tanh.md +++ b/docs/php/builtins/math/tanh.md @@ -2,7 +2,7 @@ title: "tanh()" description: "Returns the hyperbolic tangent of a number." sidebar: - order: 289 + order: 290 --- ## tanh() diff --git a/docs/php/builtins/misc/buffer_new.md b/docs/php/builtins/misc/buffer_new.md index b29c5cf732..42e79bef8d 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: 290 + order: 291 --- ## buffer_new() diff --git a/docs/php/builtins/misc/define.md b/docs/php/builtins/misc/define.md index c4fcdf8c30..755cf75ebd 100644 --- a/docs/php/builtins/misc/define.md +++ b/docs/php/builtins/misc/define.md @@ -2,7 +2,7 @@ title: "define()" description: "Defines a named constant at runtime." sidebar: - order: 291 + order: 292 --- ## define() diff --git a/docs/php/builtins/misc/defined.md b/docs/php/builtins/misc/defined.md index d0788880b0..3ec84fab03 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: 292 + order: 293 --- ## defined() diff --git a/docs/php/builtins/misc/empty.md b/docs/php/builtins/misc/empty.md index ee958199ad..096624df93 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: 293 + order: 294 --- ## empty() diff --git a/docs/php/builtins/misc/header.md b/docs/php/builtins/misc/header.md index e384ecd1f5..6a9770e54e 100644 --- a/docs/php/builtins/misc/header.md +++ b/docs/php/builtins/misc/header.md @@ -2,7 +2,7 @@ title: "header()" description: "Sends a raw HTTP header." sidebar: - order: 294 + order: 295 --- ## header() diff --git a/docs/php/builtins/misc/http_response_code.md b/docs/php/builtins/misc/http_response_code.md index 05a051758f..8f0f03afa8 100644 --- a/docs/php/builtins/misc/http_response_code.md +++ b/docs/php/builtins/misc/http_response_code.md @@ -2,7 +2,7 @@ title: "http_response_code()" description: "Gets or sets the HTTP response code." sidebar: - order: 295 + order: 296 --- ## http_response_code() diff --git a/docs/php/builtins/misc/isset.md b/docs/php/builtins/misc/isset.md index dd418f1c3c..c55ae34396 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: 296 + order: 297 --- ## isset() diff --git a/docs/php/builtins/misc/php_uname.md b/docs/php/builtins/misc/php_uname.md index b73e35fd85..f60675a8ff 100644 --- a/docs/php/builtins/misc/php_uname.md +++ b/docs/php/builtins/misc/php_uname.md @@ -2,7 +2,7 @@ title: "php_uname()" description: "Returns information about the operating system PHP is running on." sidebar: - order: 297 + order: 298 --- ## php_uname() diff --git a/docs/php/builtins/misc/phpversion.md b/docs/php/builtins/misc/phpversion.md index 4e435744d4..4650a33d09 100644 --- a/docs/php/builtins/misc/phpversion.md +++ b/docs/php/builtins/misc/phpversion.md @@ -2,7 +2,7 @@ title: "phpversion()" description: "Returns the current PHP version information." sidebar: - order: 298 + order: 299 --- ## phpversion() diff --git a/docs/php/builtins/misc/print_r.md b/docs/php/builtins/misc/print_r.md index 4c5d16d10b..bdff9a3e08 100644 --- a/docs/php/builtins/misc/print_r.md +++ b/docs/php/builtins/misc/print_r.md @@ -2,7 +2,7 @@ title: "print_r()" description: "Prints human-readable information about a variable." sidebar: - order: 299 + order: 300 --- ## print_r() diff --git a/docs/php/builtins/misc/serialize.md b/docs/php/builtins/misc/serialize.md index ffbaf184ad..55da374c71 100644 --- a/docs/php/builtins/misc/serialize.md +++ b/docs/php/builtins/misc/serialize.md @@ -2,7 +2,7 @@ title: "serialize()" description: "Generates a storable representation of a value." sidebar: - order: 300 + order: 301 --- ## serialize() diff --git a/docs/php/builtins/misc/unserialize.md b/docs/php/builtins/misc/unserialize.md index 4a270efe86..18067386b4 100644 --- a/docs/php/builtins/misc/unserialize.md +++ b/docs/php/builtins/misc/unserialize.md @@ -2,7 +2,7 @@ title: "unserialize()" description: "Creates a PHP value from a stored representation." sidebar: - order: 301 + order: 302 --- ## unserialize() diff --git a/docs/php/builtins/misc/unset.md b/docs/php/builtins/misc/unset.md index 545ddaf1c8..8d781bb955 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: 302 + order: 303 --- ## unset() diff --git a/docs/php/builtins/misc/var_dump.md b/docs/php/builtins/misc/var_dump.md index 219bf0c439..f916737947 100644 --- a/docs/php/builtins/misc/var_dump.md +++ b/docs/php/builtins/misc/var_dump.md @@ -2,7 +2,7 @@ title: "var_dump()" description: "Dumps information about a variable, including its type and value." sidebar: - order: 303 + order: 304 --- ## var_dump() diff --git a/docs/php/builtins/pointer/ptr.md b/docs/php/builtins/pointer/ptr.md index 6d4abf51ef..4dc19269d5 100644 --- a/docs/php/builtins/pointer/ptr.md +++ b/docs/php/builtins/pointer/ptr.md @@ -2,7 +2,7 @@ title: "ptr()" description: "Returns a raw pointer to the given variable." sidebar: - order: 304 + order: 305 --- ## ptr() diff --git a/docs/php/builtins/pointer/ptr_get.md b/docs/php/builtins/pointer/ptr_get.md index bca4d0bc35..487f1b26e0 100644 --- a/docs/php/builtins/pointer/ptr_get.md +++ b/docs/php/builtins/pointer/ptr_get.md @@ -2,7 +2,7 @@ title: "ptr_get()" description: "Reads one machine word through a raw pointer and returns it as an integer." sidebar: - order: 305 + order: 306 --- ## ptr_get() diff --git a/docs/php/builtins/pointer/ptr_is_null.md b/docs/php/builtins/pointer/ptr_is_null.md index 4754a90cd0..c94036561e 100644 --- a/docs/php/builtins/pointer/ptr_is_null.md +++ b/docs/php/builtins/pointer/ptr_is_null.md @@ -2,7 +2,7 @@ title: "ptr_is_null()" description: "Returns true if the pointer is null." sidebar: - order: 306 + order: 307 --- ## ptr_is_null() diff --git a/docs/php/builtins/pointer/ptr_null.md b/docs/php/builtins/pointer/ptr_null.md index baf8ae8ffc..eb63d65b87 100644 --- a/docs/php/builtins/pointer/ptr_null.md +++ b/docs/php/builtins/pointer/ptr_null.md @@ -2,7 +2,7 @@ title: "ptr_null()" description: "Returns a null raw pointer." sidebar: - order: 307 + order: 308 --- ## ptr_null() diff --git a/docs/php/builtins/pointer/ptr_offset.md b/docs/php/builtins/pointer/ptr_offset.md index 8c2452ebf0..0ef6237c6c 100644 --- a/docs/php/builtins/pointer/ptr_offset.md +++ b/docs/php/builtins/pointer/ptr_offset.md @@ -2,7 +2,7 @@ title: "ptr_offset()" description: "Returns a new pointer offset from the given pointer by the given byte count." sidebar: - order: 308 + order: 309 --- ## ptr_offset() diff --git a/docs/php/builtins/pointer/ptr_read16.md b/docs/php/builtins/pointer/ptr_read16.md index e983bc8395..19bd524fca 100644 --- a/docs/php/builtins/pointer/ptr_read16.md +++ b/docs/php/builtins/pointer/ptr_read16.md @@ -2,7 +2,7 @@ title: "ptr_read16()" description: "Reads one unsigned 16-bit word through a raw pointer and returns it as an integer." sidebar: - order: 309 + order: 310 --- ## ptr_read16() diff --git a/docs/php/builtins/pointer/ptr_read32.md b/docs/php/builtins/pointer/ptr_read32.md index e2ed85a37b..ecc181c4a3 100644 --- a/docs/php/builtins/pointer/ptr_read32.md +++ b/docs/php/builtins/pointer/ptr_read32.md @@ -2,7 +2,7 @@ title: "ptr_read32()" description: "Reads one unsigned 32-bit word through a raw pointer and returns it as an integer." sidebar: - order: 310 + order: 311 --- ## ptr_read32() diff --git a/docs/php/builtins/pointer/ptr_read8.md b/docs/php/builtins/pointer/ptr_read8.md index 5ff9ba0bbc..6db0edaefa 100644 --- a/docs/php/builtins/pointer/ptr_read8.md +++ b/docs/php/builtins/pointer/ptr_read8.md @@ -2,7 +2,7 @@ title: "ptr_read8()" description: "Reads one unsigned byte through a raw pointer and returns it as an integer." sidebar: - order: 311 + order: 312 --- ## ptr_read8() diff --git a/docs/php/builtins/pointer/ptr_read_string.md b/docs/php/builtins/pointer/ptr_read_string.md index 00cea61a5e..51e0c53ad7 100644 --- a/docs/php/builtins/pointer/ptr_read_string.md +++ b/docs/php/builtins/pointer/ptr_read_string.md @@ -2,7 +2,7 @@ title: "ptr_read_string()" description: "Copies raw bytes from a pointer into a PHP string of the given length." sidebar: - order: 312 + order: 313 --- ## ptr_read_string() diff --git a/docs/php/builtins/pointer/ptr_set.md b/docs/php/builtins/pointer/ptr_set.md index f3d0ed9839..7e7d1b5f42 100644 --- a/docs/php/builtins/pointer/ptr_set.md +++ b/docs/php/builtins/pointer/ptr_set.md @@ -2,7 +2,7 @@ title: "ptr_set()" description: "Writes one machine word through a raw pointer." sidebar: - order: 313 + order: 314 --- ## ptr_set() diff --git a/docs/php/builtins/pointer/ptr_sizeof.md b/docs/php/builtins/pointer/ptr_sizeof.md index dbca065aae..a383580343 100644 --- a/docs/php/builtins/pointer/ptr_sizeof.md +++ b/docs/php/builtins/pointer/ptr_sizeof.md @@ -2,7 +2,7 @@ title: "ptr_sizeof()" description: "Returns the byte size of the named pointer target type." sidebar: - order: 314 + order: 315 --- ## ptr_sizeof() diff --git a/docs/php/builtins/pointer/ptr_write16.md b/docs/php/builtins/pointer/ptr_write16.md index c36f7275f9..c41c179e28 100644 --- a/docs/php/builtins/pointer/ptr_write16.md +++ b/docs/php/builtins/pointer/ptr_write16.md @@ -2,7 +2,7 @@ title: "ptr_write16()" description: "Writes one 16-bit word through a raw pointer." sidebar: - order: 315 + order: 316 --- ## ptr_write16() diff --git a/docs/php/builtins/pointer/ptr_write32.md b/docs/php/builtins/pointer/ptr_write32.md index da758557ec..e9e899043f 100644 --- a/docs/php/builtins/pointer/ptr_write32.md +++ b/docs/php/builtins/pointer/ptr_write32.md @@ -2,7 +2,7 @@ title: "ptr_write32()" description: "Writes one 32-bit word through a raw pointer." sidebar: - order: 316 + order: 317 --- ## ptr_write32() diff --git a/docs/php/builtins/pointer/ptr_write8.md b/docs/php/builtins/pointer/ptr_write8.md index c89f7c0975..cbba3b3ce4 100644 --- a/docs/php/builtins/pointer/ptr_write8.md +++ b/docs/php/builtins/pointer/ptr_write8.md @@ -2,7 +2,7 @@ title: "ptr_write8()" description: "Writes one byte through a raw pointer." sidebar: - order: 317 + order: 318 --- ## ptr_write8() diff --git a/docs/php/builtins/pointer/ptr_write_string.md b/docs/php/builtins/pointer/ptr_write_string.md index c90216c4a6..204a8023b3 100644 --- a/docs/php/builtins/pointer/ptr_write_string.md +++ b/docs/php/builtins/pointer/ptr_write_string.md @@ -2,7 +2,7 @@ title: "ptr_write_string()" description: "Copies PHP string bytes into raw memory at the given pointer." sidebar: - order: 318 + order: 319 --- ## ptr_write_string() diff --git a/docs/php/builtins/pointer/zval_free.md b/docs/php/builtins/pointer/zval_free.md index 2f01cbffe5..e9fc5d0c10 100644 --- a/docs/php/builtins/pointer/zval_free.md +++ b/docs/php/builtins/pointer/zval_free.md @@ -2,7 +2,7 @@ title: "zval_free()" description: "Frees a PHP zval pointer allocated by `zval_pack`." sidebar: - order: 319 + order: 320 --- ## zval_free() diff --git a/docs/php/builtins/pointer/zval_pack.md b/docs/php/builtins/pointer/zval_pack.md index bba8dff016..6de43c3669 100644 --- a/docs/php/builtins/pointer/zval_pack.md +++ b/docs/php/builtins/pointer/zval_pack.md @@ -2,7 +2,7 @@ title: "zval_pack()" description: "Packs an elephc runtime value into a heap-allocated PHP zval pointer." sidebar: - order: 320 + order: 321 --- ## zval_pack() diff --git a/docs/php/builtins/pointer/zval_type.md b/docs/php/builtins/pointer/zval_type.md index 6a164185b3..f63e954d96 100644 --- a/docs/php/builtins/pointer/zval_type.md +++ b/docs/php/builtins/pointer/zval_type.md @@ -2,7 +2,7 @@ title: "zval_type()" description: "Returns the PHP zval type byte for a zval pointer." sidebar: - order: 321 + order: 322 --- ## zval_type() diff --git a/docs/php/builtins/pointer/zval_unpack.md b/docs/php/builtins/pointer/zval_unpack.md index a08ee17fdf..71887c84d1 100644 --- a/docs/php/builtins/pointer/zval_unpack.md +++ b/docs/php/builtins/pointer/zval_unpack.md @@ -2,7 +2,7 @@ title: "zval_unpack()" description: "Unpacks a PHP zval pointer into an owned elephc Mixed value." sidebar: - order: 322 + order: 323 --- ## zval_unpack() diff --git a/docs/php/builtins/process.md b/docs/php/builtins/process.md index d1d1f080bc..5c6bc71070 100644 --- a/docs/php/builtins/process.md +++ b/docs/php/builtins/process.md @@ -15,6 +15,10 @@ sidebar: | [`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` | ✓ | ✓ | +| [`proc_close()`](./process/proc_close.md) | `(resource $process): int` | `int` | ✓ | ✓ | +| [`proc_get_status()`](./process/proc_get_status.md) | `(resource $process): array|false` | `array|false` | ✓ | ✓ | +| [`proc_open()`](./process/proc_open.md) | `(array|string $command, array $descriptor_spec, array &$pipes, ?string $cwd = null, ?array $env_vars = null, ?array $options = null): resource|false` | `resource|false` | ✓ | ✓ | +| [`proc_terminate()`](./process/proc_terminate.md) | `(resource $process, int $signal = 15): bool` | `bool` | ✓ | ✓ | | [`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` | ✓ | ✓ | diff --git a/docs/php/builtins/process/die.md b/docs/php/builtins/process/die.md index e33dc2d7c7..46e741a2b8 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: 323 + order: 324 --- ## die() diff --git a/docs/php/builtins/process/exec.md b/docs/php/builtins/process/exec.md index bbe5a57285..359066d68d 100644 --- a/docs/php/builtins/process/exec.md +++ b/docs/php/builtins/process/exec.md @@ -2,7 +2,7 @@ title: "exec()" description: "Executes an external program and returns the last line of output." sidebar: - order: 324 + order: 325 --- ## exec() diff --git a/docs/php/builtins/process/exit.md b/docs/php/builtins/process/exit.md index 1cd20f5fb5..66247b92ac 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: 325 + order: 326 --- ## exit() diff --git a/docs/php/builtins/process/passthru.md b/docs/php/builtins/process/passthru.md index 2b1eb982dd..49328458cb 100644 --- a/docs/php/builtins/process/passthru.md +++ b/docs/php/builtins/process/passthru.md @@ -2,7 +2,7 @@ title: "passthru()" description: "Executes an external program and passes its output directly." sidebar: - order: 326 + order: 327 --- ## passthru() diff --git a/docs/php/builtins/process/pclose.md b/docs/php/builtins/process/pclose.md index a2288f0a52..fc5c3a9fa9 100644 --- a/docs/php/builtins/process/pclose.md +++ b/docs/php/builtins/process/pclose.md @@ -2,7 +2,7 @@ title: "pclose()" description: "Closes process file pointer." sidebar: - order: 327 + order: 328 --- ## pclose() diff --git a/docs/php/builtins/process/popen.md b/docs/php/builtins/process/popen.md index 59848390f0..f0b9bf9cfa 100644 --- a/docs/php/builtins/process/popen.md +++ b/docs/php/builtins/process/popen.md @@ -2,7 +2,7 @@ title: "popen()" description: "Opens process file pointer." sidebar: - order: 328 + order: 329 --- ## popen() diff --git a/docs/php/builtins/process/proc_close.md b/docs/php/builtins/process/proc_close.md new file mode 100644 index 0000000000..8d2bf7d326 --- /dev/null +++ b/docs/php/builtins/process/proc_close.md @@ -0,0 +1,36 @@ +--- +title: "proc_close()" +description: "Close a process opened by proc_open and return the exit status." +sidebar: + order: 330 +--- + +## proc_close() + +```php +function proc_close(resource $process): int +``` + +Close a process opened by proc_open and return the exit status. + +**Parameters**: +- `$process` (`resource`) + +**Returns**: `int` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/proc_close.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/proc_close.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `proc_close` is implemented in the compiler, see [the internals page](../../../internals/builtins/process/proc_close.md). diff --git a/docs/php/builtins/process/proc_get_status.md b/docs/php/builtins/process/proc_get_status.md new file mode 100644 index 0000000000..1d654a41e6 --- /dev/null +++ b/docs/php/builtins/process/proc_get_status.md @@ -0,0 +1,39 @@ +--- +title: "proc_get_status()" +description: "Retrieves the current status of a process opened by proc_open." +sidebar: + order: 331 +--- + +## proc_get_status() + +```php +function proc_get_status(resource $process): array|false +``` + +Retrieves the current status of a process opened by proc_open. + +**Parameters**: +- `$process` (`resource`) + +**Returns**: `array|false` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/proc_get_status.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/proc_get_status.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + +**Notes**: +- The status record contains `command`, `pid`, `cached`, `running`, `signaled`, `stopped`, `exitcode`, `termsig`, and `stopsig`. +- Windows status queries do not reap the process and report `cached` as `false`. +- Unix caches a normally exited child so `proc_close()` can still return its exit code. + + + + +## Internals + +For how `proc_get_status` is implemented in the compiler, see [the internals page](../../../internals/builtins/process/proc_get_status.md). diff --git a/docs/php/builtins/process/proc_open.md b/docs/php/builtins/process/proc_open.md new file mode 100644 index 0000000000..f964ba4279 --- /dev/null +++ b/docs/php/builtins/process/proc_open.md @@ -0,0 +1,46 @@ +--- +title: "proc_open()" +description: "Execute a command and open file pointers for process I/O." +sidebar: + order: 332 +--- + +## proc_open() + +```php +function proc_open(array|string $command, array $descriptor_spec, array &$pipes, ?string $cwd = null, ?array $env_vars = null, ?array $options = null): resource|false +``` + +Execute a command and open file pointers for process I/O. + +**Parameters**: +- `$command` (`array|string`) +- `$descriptor_spec` (`array`) +- `$pipes` (`array`), passed by reference +- `$cwd` (`?string`), default `null`, optional +- `$env_vars` (`?array`), default `null`, optional +- `$options` (`?array`), default `null`, optional + +**Returns**: `resource|false` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/proc_open.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/proc_open.rs)). + +**Examples**: + +See [`examples/process-pipes`](../../../../examples/process-pipes/main.php) for a complete stdin/stdout/stderr exchange and process-status query. + +**Notes**: +- Windows accepts string and array commands, UTF-8 working directories and environment maps, plus PHP's documented process options. +- The Windows descriptor runtime supports `pipe`, `socket`, `file`, `redirect`, stream-resource, and `null` entries while preserving sparse integer keys. +- Descriptors above 2 are rejected on Windows because `STARTUPINFOW` cannot expose them as matching numbered CRT descriptors. +- The Windows `blocking_pipes` option selects blocking `ReadFile` behavior; the default probes readable pipes and reports `EAGAIN` when no bytes are available. + + + + +## Internals + +For how `proc_open` is implemented in the compiler, see [the internals page](../../../internals/builtins/process/proc_open.md). diff --git a/docs/php/builtins/process/proc_terminate.md b/docs/php/builtins/process/proc_terminate.md new file mode 100644 index 0000000000..115f7c65bd --- /dev/null +++ b/docs/php/builtins/process/proc_terminate.md @@ -0,0 +1,39 @@ +--- +title: "proc_terminate()" +description: "Terminates a process opened by proc_open." +sidebar: + order: 333 +--- + +## proc_terminate() + +```php +function proc_terminate(resource $process, int $signal = 15): bool +``` + +Terminates a process opened by proc_open. + +**Parameters**: +- `$process` (`resource`) +- `$signal` (`int`), default `15`, optional + +**Returns**: `bool` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/proc_terminate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/proc_terminate.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + +**Notes**: +- Unix forwards the optional signal to `kill(2)`. +- Windows follows PHP by ignoring the signal value and terminating the process with exit code 255. + + + + +## Internals + +For how `proc_terminate` is implemented in the compiler, see [the internals page](../../../internals/builtins/process/proc_terminate.md). diff --git a/docs/php/builtins/process/readline.md b/docs/php/builtins/process/readline.md index 35eb367e81..7273d938f4 100644 --- a/docs/php/builtins/process/readline.md +++ b/docs/php/builtins/process/readline.md @@ -2,7 +2,7 @@ title: "readline()" description: "Reads a line from the user's terminal." sidebar: - order: 329 + order: 334 --- ## readline() diff --git a/docs/php/builtins/process/shell_exec.md b/docs/php/builtins/process/shell_exec.md index 9e07df43ed..09892f2209 100644 --- a/docs/php/builtins/process/shell_exec.md +++ b/docs/php/builtins/process/shell_exec.md @@ -2,7 +2,7 @@ title: "shell_exec()" description: "Executes a command via the shell and returns the complete output as a string." sidebar: - order: 330 + order: 335 --- ## shell_exec() diff --git a/docs/php/builtins/process/sleep.md b/docs/php/builtins/process/sleep.md index d4658f41cd..6c1657277c 100644 --- a/docs/php/builtins/process/sleep.md +++ b/docs/php/builtins/process/sleep.md @@ -2,7 +2,7 @@ title: "sleep()" description: "Delays execution for a number of seconds." sidebar: - order: 331 + order: 336 --- ## sleep() diff --git a/docs/php/builtins/process/system.md b/docs/php/builtins/process/system.md index feb3e39738..751d44295d 100644 --- a/docs/php/builtins/process/system.md +++ b/docs/php/builtins/process/system.md @@ -2,7 +2,7 @@ title: "system()" description: "Executes an external program and displays the output." sidebar: - order: 332 + order: 337 --- ## system() diff --git a/docs/php/builtins/process/usleep.md b/docs/php/builtins/process/usleep.md index c82abc09d6..7526f24d17 100644 --- a/docs/php/builtins/process/usleep.md +++ b/docs/php/builtins/process/usleep.md @@ -2,7 +2,7 @@ title: "usleep()" description: "Delays execution for a number of microseconds." sidebar: - order: 333 + order: 338 --- ## usleep() diff --git a/docs/php/builtins/regex.md b/docs/php/builtins/regex.md index 266750173f..5b8f159b54 100644 --- a/docs/php/builtins/regex.md +++ b/docs/php/builtins/regex.md @@ -10,7 +10,7 @@ sidebar: | Function | Signature | Returns | AOT | eval() | |---|---|---|:-:|:-:| | [`mb_ereg_match()`](./regex/mb_ereg_match.md) | `(string $pattern, string $subject, string $options = null): bool` | `bool` | ✓ | ✓ | -| [`preg_match()`](./regex/preg_match.md) | `(string $pattern, string $subject, array $matches = []): int` | `int` | ✓ | ✓ | +| [`preg_match()`](./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` | ✓ | ✓ | diff --git a/docs/php/builtins/regex/mb_ereg_match.md b/docs/php/builtins/regex/mb_ereg_match.md index 5789623699..6e58ae3127 100644 --- a/docs/php/builtins/regex/mb_ereg_match.md +++ b/docs/php/builtins/regex/mb_ereg_match.md @@ -2,7 +2,7 @@ title: "mb_ereg_match()" description: "Tests whether a regex pattern matches the beginning of a string (multibyte)." sidebar: - order: 334 + order: 339 --- ## mb_ereg_match() diff --git a/docs/php/builtins/regex/preg_match.md b/docs/php/builtins/regex/preg_match.md index 5d28ac83de..df5eaea766 100644 --- a/docs/php/builtins/regex/preg_match.md +++ b/docs/php/builtins/regex/preg_match.md @@ -2,13 +2,13 @@ title: "preg_match()" description: "Performs a regular expression match." sidebar: - order: 335 + order: 340 --- ## preg_match() ```php -function preg_match(string $pattern, string $subject, array $matches = []): int +function preg_match(string $pattern, string $subject, array &$matches = []): int ``` Performs a regular expression match. diff --git a/docs/php/builtins/regex/preg_match_all.md b/docs/php/builtins/regex/preg_match_all.md index 78feb7ebe3..c142b257b4 100644 --- a/docs/php/builtins/regex/preg_match_all.md +++ b/docs/php/builtins/regex/preg_match_all.md @@ -2,7 +2,7 @@ title: "preg_match_all()" description: "Performs a global regular expression match and returns the number of matches." sidebar: - order: 336 + order: 341 --- ## preg_match_all() diff --git a/docs/php/builtins/regex/preg_replace.md b/docs/php/builtins/regex/preg_replace.md index a8b0bb3024..6582929135 100644 --- a/docs/php/builtins/regex/preg_replace.md +++ b/docs/php/builtins/regex/preg_replace.md @@ -2,7 +2,7 @@ title: "preg_replace()" description: "Performs a regular expression search and replace." sidebar: - order: 337 + order: 342 --- ## preg_replace() diff --git a/docs/php/builtins/regex/preg_replace_callback.md b/docs/php/builtins/regex/preg_replace_callback.md index ccc7309bf9..91380e259b 100644 --- a/docs/php/builtins/regex/preg_replace_callback.md +++ b/docs/php/builtins/regex/preg_replace_callback.md @@ -2,7 +2,7 @@ title: "preg_replace_callback()" description: "Performs a regular expression search and replace using a callback." sidebar: - order: 338 + order: 343 --- ## preg_replace_callback() diff --git a/docs/php/builtins/regex/preg_split.md b/docs/php/builtins/regex/preg_split.md index c2a8449522..95c43b51bc 100644 --- a/docs/php/builtins/regex/preg_split.md +++ b/docs/php/builtins/regex/preg_split.md @@ -2,7 +2,7 @@ title: "preg_split()" description: "Splits a string by a regular expression." sidebar: - order: 339 + order: 344 --- ## preg_split() diff --git a/docs/php/builtins/spl/iterator_apply.md b/docs/php/builtins/spl/iterator_apply.md index 90b85db022..268193629e 100644 --- a/docs/php/builtins/spl/iterator_apply.md +++ b/docs/php/builtins/spl/iterator_apply.md @@ -2,7 +2,7 @@ title: "iterator_apply()" description: "Call a function for every element in an iterator." sidebar: - order: 340 + order: 345 --- ## iterator_apply() diff --git a/docs/php/builtins/spl/iterator_count.md b/docs/php/builtins/spl/iterator_count.md index 484842c2a9..02a79f5bb8 100644 --- a/docs/php/builtins/spl/iterator_count.md +++ b/docs/php/builtins/spl/iterator_count.md @@ -2,7 +2,7 @@ title: "iterator_count()" description: "Count the elements in an iterator." sidebar: - order: 341 + order: 346 --- ## iterator_count() diff --git a/docs/php/builtins/spl/iterator_to_array.md b/docs/php/builtins/spl/iterator_to_array.md index 562b7addd2..38cb732dd0 100644 --- a/docs/php/builtins/spl/iterator_to_array.md +++ b/docs/php/builtins/spl/iterator_to_array.md @@ -2,7 +2,7 @@ title: "iterator_to_array()" description: "Copy the iterator into an array." sidebar: - order: 342 + order: 347 --- ## iterator_to_array() diff --git a/docs/php/builtins/spl/spl_autoload.md b/docs/php/builtins/spl/spl_autoload.md index 6afbe28bb9..9c106f7c48 100644 --- a/docs/php/builtins/spl/spl_autoload.md +++ b/docs/php/builtins/spl/spl_autoload.md @@ -2,7 +2,7 @@ title: "spl_autoload()" description: "Default implementation for __autoload()." sidebar: - order: 343 + order: 348 --- ## spl_autoload() diff --git a/docs/php/builtins/spl/spl_autoload_call.md b/docs/php/builtins/spl/spl_autoload_call.md index f64f27d96c..b183d0810f 100644 --- a/docs/php/builtins/spl/spl_autoload_call.md +++ b/docs/php/builtins/spl/spl_autoload_call.md @@ -2,7 +2,7 @@ title: "spl_autoload_call()" description: "Try all registered __autoload() functions to load the requested class." sidebar: - order: 344 + order: 349 --- ## spl_autoload_call() diff --git a/docs/php/builtins/spl/spl_autoload_extensions.md b/docs/php/builtins/spl/spl_autoload_extensions.md index d88ed6df88..3ee2134fdd 100644 --- a/docs/php/builtins/spl/spl_autoload_extensions.md +++ b/docs/php/builtins/spl/spl_autoload_extensions.md @@ -2,7 +2,7 @@ title: "spl_autoload_extensions()" description: "Register and return default file extensions for spl_autoload." sidebar: - order: 345 + order: 350 --- ## spl_autoload_extensions() diff --git a/docs/php/builtins/spl/spl_autoload_functions.md b/docs/php/builtins/spl/spl_autoload_functions.md index c90656ba5e..6d7f2c77a7 100644 --- a/docs/php/builtins/spl/spl_autoload_functions.md +++ b/docs/php/builtins/spl/spl_autoload_functions.md @@ -2,7 +2,7 @@ title: "spl_autoload_functions()" description: "Return all registered __autoload() functions." sidebar: - order: 346 + order: 351 --- ## spl_autoload_functions() diff --git a/docs/php/builtins/spl/spl_autoload_register.md b/docs/php/builtins/spl/spl_autoload_register.md index b2808e05a8..af00a8b7aa 100644 --- a/docs/php/builtins/spl/spl_autoload_register.md +++ b/docs/php/builtins/spl/spl_autoload_register.md @@ -2,7 +2,7 @@ title: "spl_autoload_register()" description: "Register given function as __autoload() implementation." sidebar: - order: 347 + order: 352 --- ## spl_autoload_register() diff --git a/docs/php/builtins/spl/spl_autoload_unregister.md b/docs/php/builtins/spl/spl_autoload_unregister.md index 606fa8c208..a2848f15b4 100644 --- a/docs/php/builtins/spl/spl_autoload_unregister.md +++ b/docs/php/builtins/spl/spl_autoload_unregister.md @@ -2,7 +2,7 @@ title: "spl_autoload_unregister()" description: "Unregister given function as __autoload() implementation." sidebar: - order: 348 + order: 353 --- ## spl_autoload_unregister() diff --git a/docs/php/builtins/spl/spl_classes.md b/docs/php/builtins/spl/spl_classes.md index 655663b970..049565573a 100644 --- a/docs/php/builtins/spl/spl_classes.md +++ b/docs/php/builtins/spl/spl_classes.md @@ -2,7 +2,7 @@ title: "spl_classes()" description: "Return available SPL classes." sidebar: - order: 349 + order: 354 --- ## spl_classes() diff --git a/docs/php/builtins/spl/spl_object_hash.md b/docs/php/builtins/spl/spl_object_hash.md index 2a0d790b48..9509deebc8 100644 --- a/docs/php/builtins/spl/spl_object_hash.md +++ b/docs/php/builtins/spl/spl_object_hash.md @@ -2,7 +2,7 @@ title: "spl_object_hash()" description: "Return hash id for given object." sidebar: - order: 350 + order: 355 --- ## spl_object_hash() diff --git a/docs/php/builtins/spl/spl_object_id.md b/docs/php/builtins/spl/spl_object_id.md index 3d47a09fc9..a96cbd361a 100644 --- a/docs/php/builtins/spl/spl_object_id.md +++ b/docs/php/builtins/spl/spl_object_id.md @@ -2,7 +2,7 @@ title: "spl_object_id()" description: "Return the integer object handle for given object." sidebar: - order: 351 + order: 356 --- ## spl_object_id() diff --git a/docs/php/builtins/streams.md b/docs/php/builtins/streams.md index 4bf6d7b2a6..d322b693a6 100644 --- a/docs/php/builtins/streams.md +++ b/docs/php/builtins/streams.md @@ -9,8 +9,8 @@ sidebar: | Function | Signature | Returns | AOT | eval() | |---|---|---|:-:|:-:| -| [`fsockopen()`](./streams/fsockopen.md) | `(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed` | `mixed` | ✓ | ✓ | -| [`pfsockopen()`](./streams/pfsockopen.md) | `(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed` | `mixed` | ✓ | ✓ | +| [`fsockopen()`](./streams/fsockopen.md) | `(string $hostname, int $port, int &$error_code = null, string &$error_message = null, float $timeout = null): mixed` | `mixed` | ✓ | ✓ | +| [`pfsockopen()`](./streams/pfsockopen.md) | `(string $hostname, int $port, int &$error_code = null, string &$error_message = null, float $timeout = null): mixed` | `mixed` | ✓ | ✓ | | [`stream_bucket_append()`](./streams/stream_bucket_append.md) | `(mixed $brigade, mixed $bucket): void` | `void` | ✓ | ✓ | | [`stream_bucket_prepend()`](./streams/stream_bucket_prepend.md) | `(mixed $brigade, mixed $bucket): void` | `void` | ✓ | ✓ | | [`stream_filter_append()`](./streams/stream_filter_append.md) | `(resource $stream, string $filtername, int $read_write = 3, mixed $params = null): mixed` | `mixed` | ✓ | ✓ | diff --git a/docs/php/builtins/streams/fsockopen.md b/docs/php/builtins/streams/fsockopen.md index d8a16539d1..fdac129ec6 100644 --- a/docs/php/builtins/streams/fsockopen.md +++ b/docs/php/builtins/streams/fsockopen.md @@ -2,13 +2,13 @@ title: "fsockopen()" description: "Open Internet or Unix domain socket connection." sidebar: - order: 352 + order: 357 --- ## fsockopen() ```php -function fsockopen(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed +function fsockopen(string $hostname, int $port, int &$error_code = null, string &$error_message = null, float $timeout = null): mixed ``` Open Internet or Unix domain socket connection. diff --git a/docs/php/builtins/streams/pfsockopen.md b/docs/php/builtins/streams/pfsockopen.md index 036964b7eb..82b65934eb 100644 --- a/docs/php/builtins/streams/pfsockopen.md +++ b/docs/php/builtins/streams/pfsockopen.md @@ -2,13 +2,13 @@ title: "pfsockopen()" description: "Open persistent Internet or Unix domain socket connection." sidebar: - order: 353 + order: 358 --- ## pfsockopen() ```php -function pfsockopen(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed +function pfsockopen(string $hostname, int $port, int &$error_code = null, string &$error_message = null, float $timeout = null): mixed ``` Open persistent Internet or Unix domain socket connection. diff --git a/docs/php/builtins/streams/stream_bucket_append.md b/docs/php/builtins/streams/stream_bucket_append.md index 0d9202d2d7..c02d55fb51 100644 --- a/docs/php/builtins/streams/stream_bucket_append.md +++ b/docs/php/builtins/streams/stream_bucket_append.md @@ -2,7 +2,7 @@ title: "stream_bucket_append()" description: "Appends a bucket to the brigade." sidebar: - order: 354 + order: 359 --- ## stream_bucket_append() diff --git a/docs/php/builtins/streams/stream_bucket_prepend.md b/docs/php/builtins/streams/stream_bucket_prepend.md index e1038e6e87..e91d49ffff 100644 --- a/docs/php/builtins/streams/stream_bucket_prepend.md +++ b/docs/php/builtins/streams/stream_bucket_prepend.md @@ -2,7 +2,7 @@ title: "stream_bucket_prepend()" description: "Prepends a bucket to the brigade." sidebar: - order: 355 + order: 360 --- ## stream_bucket_prepend() diff --git a/docs/php/builtins/streams/stream_filter_append.md b/docs/php/builtins/streams/stream_filter_append.md index 44eefb3c33..9ef7ec6ed0 100644 --- a/docs/php/builtins/streams/stream_filter_append.md +++ b/docs/php/builtins/streams/stream_filter_append.md @@ -2,7 +2,7 @@ title: "stream_filter_append()" description: "Attaches a filter to a stream." sidebar: - order: 356 + order: 361 --- ## stream_filter_append() diff --git a/docs/php/builtins/streams/stream_filter_prepend.md b/docs/php/builtins/streams/stream_filter_prepend.md index 0f8ecf9b7f..62ea4f4473 100644 --- a/docs/php/builtins/streams/stream_filter_prepend.md +++ b/docs/php/builtins/streams/stream_filter_prepend.md @@ -2,7 +2,7 @@ title: "stream_filter_prepend()" description: "Attaches a filter to a stream (prepend)." sidebar: - order: 357 + order: 362 --- ## stream_filter_prepend() diff --git a/docs/php/builtins/string.md b/docs/php/builtins/string.md index 2458ec39de..79091badbb 100644 --- a/docs/php/builtins/string.md +++ b/docs/php/builtins/string.md @@ -16,6 +16,8 @@ sidebar: | [`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` | ✓ | ✓ | +| [`escapeshellarg()`](./string/escapeshellarg.md) | `(string $arg): string` | `string` | ✓ | ✓ | +| [`escapeshellcmd()`](./string/escapeshellcmd.md) | `(string $command): string` | `string` | ✓ | ✓ | | [`explode()`](./string/explode.md) | `(string $separator, string $string, int $limit = PHP_INT_MAX): array` | `array` | ✓ | ✓ | | [`grapheme_strrev()`](./string/grapheme_strrev.md) | `(string $string): mixed` | `mixed` | ✓ | ✓ | | [`gzcompress()`](./string/gzcompress.md) | `(string $data, int $level = -1): string` | `string` | ✓ | ✓ | @@ -41,7 +43,7 @@ sidebar: | [`lcfirst()`](./string/lcfirst.md) | `(string $string): string` | `string` | ✓ | ✓ | | [`long2ip()`](./string/long2ip.md) | `(int $ip): string` | `string` | ✓ | ✓ | | [`ltrim()`](./string/ltrim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | -| [`mb_strlen()`](./string/mb_strlen.md) | `(string $string, string $encoding = null): int` | `int` | ✓ | ✓ | +| [`mb_strlen()`](./string/mb_strlen.md) | `(string $string, mixed $encoding = null): int` | `int` | ✓ | ✓ | | [`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` | ✓ | ✓ | diff --git a/docs/php/builtins/string/addslashes.md b/docs/php/builtins/string/addslashes.md index d04b4aa373..dcd09ea9e3 100644 --- a/docs/php/builtins/string/addslashes.md +++ b/docs/php/builtins/string/addslashes.md @@ -2,7 +2,7 @@ title: "addslashes()" description: "Adds backslashes before characters that need to be escaped." sidebar: - order: 358 + order: 363 --- ## addslashes() diff --git a/docs/php/builtins/string/base64_decode.md b/docs/php/builtins/string/base64_decode.md index f8c578673c..57430d3b68 100644 --- a/docs/php/builtins/string/base64_decode.md +++ b/docs/php/builtins/string/base64_decode.md @@ -2,7 +2,7 @@ title: "base64_decode()" description: "Decodes a Base64-encoded string back into its original data." sidebar: - order: 359 + order: 364 --- ## base64_decode() diff --git a/docs/php/builtins/string/base64_encode.md b/docs/php/builtins/string/base64_encode.md index e3bf5f5302..8995eea529 100644 --- a/docs/php/builtins/string/base64_encode.md +++ b/docs/php/builtins/string/base64_encode.md @@ -2,7 +2,7 @@ title: "base64_encode()" description: "Encodes binary data into a Base64 string." sidebar: - order: 360 + order: 365 --- ## base64_encode() diff --git a/docs/php/builtins/string/bin2hex.md b/docs/php/builtins/string/bin2hex.md index b54665483d..9fc360a1d4 100644 --- a/docs/php/builtins/string/bin2hex.md +++ b/docs/php/builtins/string/bin2hex.md @@ -2,7 +2,7 @@ title: "bin2hex()" description: "Converts binary data into its hexadecimal string representation." sidebar: - order: 361 + order: 366 --- ## bin2hex() diff --git a/docs/php/builtins/string/chop.md b/docs/php/builtins/string/chop.md index 6f4bbb00be..4d51821419 100644 --- a/docs/php/builtins/string/chop.md +++ b/docs/php/builtins/string/chop.md @@ -2,7 +2,7 @@ title: "chop()" description: "Alias of rtrim: strips whitespace (or other characters) from the end of a string." sidebar: - order: 362 + order: 367 --- ## chop() diff --git a/docs/php/builtins/string/chr.md b/docs/php/builtins/string/chr.md index 6adac54ab9..50800d0339 100644 --- a/docs/php/builtins/string/chr.md +++ b/docs/php/builtins/string/chr.md @@ -2,7 +2,7 @@ title: "chr()" description: "Returns a one-character string from the given byte code point." sidebar: - order: 363 + order: 368 --- ## chr() diff --git a/docs/php/builtins/string/crc32.md b/docs/php/builtins/string/crc32.md index 66b1cbfe3e..a04619d45c 100644 --- a/docs/php/builtins/string/crc32.md +++ b/docs/php/builtins/string/crc32.md @@ -2,7 +2,7 @@ title: "crc32()" description: "Calculates the CRC32 polynomial of a string." sidebar: - order: 364 + order: 369 --- ## crc32() diff --git a/docs/php/builtins/string/escapeshellarg.md b/docs/php/builtins/string/escapeshellarg.md new file mode 100644 index 0000000000..d86d46ae93 --- /dev/null +++ b/docs/php/builtins/string/escapeshellarg.md @@ -0,0 +1,40 @@ +--- +title: "escapeshellarg()" +description: "Quotes one argument for safe use in a shell command." +sidebar: + order: 370 +--- + +## escapeshellarg() + +```php +function escapeshellarg(string $arg): string +``` + +Quotes one argument for safe use in a shell command. + +**Parameters**: +- `$arg` (`string`) + +**Returns**: `string` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/escapeshellarg.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/escapeshellarg.rs)). + +**Examples**: + +See [`examples/shell-escaping`](../../../../examples/shell-escaping/main.php) for argument and command escaping together. + +**Notes**: +- macOS and Linux use PHP's single-quoted shell form; Windows uses PHP's double-quoted command-line escaping rules. +- Embedded NUL bytes raise a catchable `ValueError`. +- Windows enforces PHP's 8192-byte command-line limit for both input and output. + + + + +## Internals + +For how `escapeshellarg` is implemented in the compiler, see [the internals page](../../../internals/builtins/string/escapeshellarg.md). diff --git a/docs/php/builtins/string/escapeshellcmd.md b/docs/php/builtins/string/escapeshellcmd.md new file mode 100644 index 0000000000..d1622ca979 --- /dev/null +++ b/docs/php/builtins/string/escapeshellcmd.md @@ -0,0 +1,40 @@ +--- +title: "escapeshellcmd()" +description: "Escapes shell metacharacters in a command string." +sidebar: + order: 371 +--- + +## escapeshellcmd() + +```php +function escapeshellcmd(string $command): string +``` + +Escapes shell metacharacters in a command string. + +**Parameters**: +- `$command` (`string`) + +**Returns**: `string` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/escapeshellcmd.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/escapeshellcmd.rs)). + +**Examples**: + +See [`examples/shell-escaping`](../../../../examples/shell-escaping/main.php) for argument and command escaping together. + +**Notes**: +- Shell metacharacters follow PHP's platform-specific POSIX or Windows rules. +- Embedded NUL bytes raise a catchable `ValueError`. +- Windows enforces PHP's 8192-byte command-line limit for both input and output. + + + + +## Internals + +For how `escapeshellcmd` is implemented in the compiler, see [the internals page](../../../internals/builtins/string/escapeshellcmd.md). diff --git a/docs/php/builtins/string/explode.md b/docs/php/builtins/string/explode.md index 07221f3575..1b4936ccae 100644 --- a/docs/php/builtins/string/explode.md +++ b/docs/php/builtins/string/explode.md @@ -2,7 +2,7 @@ title: "explode()" description: "Splits a string by a separator into an array of substrings." sidebar: - order: 365 + order: 372 --- ## explode() diff --git a/docs/php/builtins/string/grapheme_strrev.md b/docs/php/builtins/string/grapheme_strrev.md index 735371e685..0091fd551d 100644 --- a/docs/php/builtins/string/grapheme_strrev.md +++ b/docs/php/builtins/string/grapheme_strrev.md @@ -2,7 +2,7 @@ title: "grapheme_strrev()" description: "Reverses a string by grapheme cluster, returning false on failure." sidebar: - order: 366 + order: 373 --- ## grapheme_strrev() diff --git a/docs/php/builtins/string/gzcompress.md b/docs/php/builtins/string/gzcompress.md index cd2c10bcee..c8c9ef3bb1 100644 --- a/docs/php/builtins/string/gzcompress.md +++ b/docs/php/builtins/string/gzcompress.md @@ -2,7 +2,7 @@ title: "gzcompress()" description: "Compress a string using the ZLIB data format." sidebar: - order: 367 + order: 374 --- ## gzcompress() diff --git a/docs/php/builtins/string/gzdeflate.md b/docs/php/builtins/string/gzdeflate.md index 56c3a88a8a..83da8d1ef0 100644 --- a/docs/php/builtins/string/gzdeflate.md +++ b/docs/php/builtins/string/gzdeflate.md @@ -2,7 +2,7 @@ title: "gzdeflate()" description: "Deflate a string using the DEFLATE data format." sidebar: - order: 368 + order: 375 --- ## gzdeflate() diff --git a/docs/php/builtins/string/gzinflate.md b/docs/php/builtins/string/gzinflate.md index 382dc503c2..ae95901280 100644 --- a/docs/php/builtins/string/gzinflate.md +++ b/docs/php/builtins/string/gzinflate.md @@ -2,7 +2,7 @@ title: "gzinflate()" description: "Inflate a deflated string." sidebar: - order: 369 + order: 376 --- ## gzinflate() diff --git a/docs/php/builtins/string/gzuncompress.md b/docs/php/builtins/string/gzuncompress.md index 41612d3172..b762e1ea46 100644 --- a/docs/php/builtins/string/gzuncompress.md +++ b/docs/php/builtins/string/gzuncompress.md @@ -2,7 +2,7 @@ title: "gzuncompress()" description: "Uncompress a compressed string." sidebar: - order: 370 + order: 377 --- ## gzuncompress() diff --git a/docs/php/builtins/string/hash.md b/docs/php/builtins/string/hash.md index ceb73a3360..5f5a636381 100644 --- a/docs/php/builtins/string/hash.md +++ b/docs/php/builtins/string/hash.md @@ -2,7 +2,7 @@ title: "hash()" description: "Generates a hash value using the given algorithm." sidebar: - order: 371 + order: 378 --- ## hash() diff --git a/docs/php/builtins/string/hash_algos.md b/docs/php/builtins/string/hash_algos.md index 8282380ce1..268f5f47cd 100644 --- a/docs/php/builtins/string/hash_algos.md +++ b/docs/php/builtins/string/hash_algos.md @@ -2,7 +2,7 @@ title: "hash_algos()" description: "Returns an array of supported hashing algorithm names." sidebar: - order: 372 + order: 379 --- ## hash_algos() diff --git a/docs/php/builtins/string/hash_copy.md b/docs/php/builtins/string/hash_copy.md index 43fefe6b2c..d983c191c9 100644 --- a/docs/php/builtins/string/hash_copy.md +++ b/docs/php/builtins/string/hash_copy.md @@ -2,7 +2,7 @@ title: "hash_copy()" description: "Copies the state of an incremental hashing context." sidebar: - order: 373 + order: 380 --- ## hash_copy() diff --git a/docs/php/builtins/string/hash_equals.md b/docs/php/builtins/string/hash_equals.md index b8bc4037bb..b96d83340e 100644 --- a/docs/php/builtins/string/hash_equals.md +++ b/docs/php/builtins/string/hash_equals.md @@ -2,7 +2,7 @@ title: "hash_equals()" description: "Compares two strings using a constant-time algorithm." sidebar: - order: 374 + order: 381 --- ## hash_equals() diff --git a/docs/php/builtins/string/hash_final.md b/docs/php/builtins/string/hash_final.md index 70e99f7649..128b750c07 100644 --- a/docs/php/builtins/string/hash_final.md +++ b/docs/php/builtins/string/hash_final.md @@ -2,7 +2,7 @@ title: "hash_final()" description: "Finalizes an incremental hash and returns the digest string." sidebar: - order: 375 + order: 382 --- ## hash_final() diff --git a/docs/php/builtins/string/hash_hmac.md b/docs/php/builtins/string/hash_hmac.md index b5ec5661e8..5da9d38403 100644 --- a/docs/php/builtins/string/hash_hmac.md +++ b/docs/php/builtins/string/hash_hmac.md @@ -2,7 +2,7 @@ title: "hash_hmac()" description: "Generates a keyed hash value using the HMAC method." sidebar: - order: 376 + order: 383 --- ## hash_hmac() diff --git a/docs/php/builtins/string/hash_init.md b/docs/php/builtins/string/hash_init.md index 509d5da44f..facfa8c52e 100644 --- a/docs/php/builtins/string/hash_init.md +++ b/docs/php/builtins/string/hash_init.md @@ -2,7 +2,7 @@ title: "hash_init()" description: "Initialize an incremental hashing context." sidebar: - order: 377 + order: 384 --- ## hash_init() diff --git a/docs/php/builtins/string/hash_update.md b/docs/php/builtins/string/hash_update.md index 3397db02e1..8ddea55393 100644 --- a/docs/php/builtins/string/hash_update.md +++ b/docs/php/builtins/string/hash_update.md @@ -2,7 +2,7 @@ title: "hash_update()" description: "Pumps data into an active incremental hashing context." sidebar: - order: 378 + order: 385 --- ## hash_update() diff --git a/docs/php/builtins/string/hex2bin.md b/docs/php/builtins/string/hex2bin.md index 7dea4fa494..aeba44e6b9 100644 --- a/docs/php/builtins/string/hex2bin.md +++ b/docs/php/builtins/string/hex2bin.md @@ -2,7 +2,7 @@ title: "hex2bin()" description: "Decodes a hexadecimal string back into its binary representation." sidebar: - order: 379 + order: 386 --- ## hex2bin() diff --git a/docs/php/builtins/string/html_entity_decode.md b/docs/php/builtins/string/html_entity_decode.md index 59d9c44625..00304f522e 100644 --- a/docs/php/builtins/string/html_entity_decode.md +++ b/docs/php/builtins/string/html_entity_decode.md @@ -2,7 +2,7 @@ title: "html_entity_decode()" description: "Converts HTML entities in a string back into their corresponding characters." sidebar: - order: 380 + order: 387 --- ## html_entity_decode() diff --git a/docs/php/builtins/string/htmlentities.md b/docs/php/builtins/string/htmlentities.md index c766928818..8b493a64d1 100644 --- a/docs/php/builtins/string/htmlentities.md +++ b/docs/php/builtins/string/htmlentities.md @@ -2,7 +2,7 @@ title: "htmlentities()" description: "Converts all applicable characters in a string into their HTML entities." sidebar: - order: 381 + order: 388 --- ## htmlentities() diff --git a/docs/php/builtins/string/htmlspecialchars.md b/docs/php/builtins/string/htmlspecialchars.md index 84d4dbcf1b..b28eb4ded5 100644 --- a/docs/php/builtins/string/htmlspecialchars.md +++ b/docs/php/builtins/string/htmlspecialchars.md @@ -2,7 +2,7 @@ title: "htmlspecialchars()" description: "Converts the HTML special characters in a string into their entities." sidebar: - order: 382 + order: 389 --- ## htmlspecialchars() diff --git a/docs/php/builtins/string/implode.md b/docs/php/builtins/string/implode.md index 5dbb4e309b..8ba78aa99e 100644 --- a/docs/php/builtins/string/implode.md +++ b/docs/php/builtins/string/implode.md @@ -2,7 +2,7 @@ title: "implode()" description: "Joins array elements into a single string using a separator." sidebar: - order: 383 + order: 390 --- ## implode() diff --git a/docs/php/builtins/string/inet_ntop.md b/docs/php/builtins/string/inet_ntop.md index 435b683934..5a7984cec7 100644 --- a/docs/php/builtins/string/inet_ntop.md +++ b/docs/php/builtins/string/inet_ntop.md @@ -2,7 +2,7 @@ title: "inet_ntop()" description: "Converts a packed internet address to a human-readable representation." sidebar: - order: 384 + order: 391 --- ## inet_ntop() diff --git a/docs/php/builtins/string/inet_pton.md b/docs/php/builtins/string/inet_pton.md index 97e774a0ad..c4accee3f9 100644 --- a/docs/php/builtins/string/inet_pton.md +++ b/docs/php/builtins/string/inet_pton.md @@ -2,7 +2,7 @@ title: "inet_pton()" description: "Converts a human-readable IP address to its packed in_addr representation." sidebar: - order: 385 + order: 392 --- ## inet_pton() diff --git a/docs/php/builtins/string/ip2long.md b/docs/php/builtins/string/ip2long.md index 082e5ae78a..b1eaed00de 100644 --- a/docs/php/builtins/string/ip2long.md +++ b/docs/php/builtins/string/ip2long.md @@ -2,7 +2,7 @@ title: "ip2long()" description: "Converts a string containing an IPv4 address into a long integer." sidebar: - order: 386 + order: 393 --- ## ip2long() diff --git a/docs/php/builtins/string/lcfirst.md b/docs/php/builtins/string/lcfirst.md index f4578b69e3..85dc1f0b74 100644 --- a/docs/php/builtins/string/lcfirst.md +++ b/docs/php/builtins/string/lcfirst.md @@ -2,7 +2,7 @@ title: "lcfirst()" description: "Lowercases the first character of a string." sidebar: - order: 387 + order: 394 --- ## lcfirst() diff --git a/docs/php/builtins/string/long2ip.md b/docs/php/builtins/string/long2ip.md index dac737807b..eae56e8d17 100644 --- a/docs/php/builtins/string/long2ip.md +++ b/docs/php/builtins/string/long2ip.md @@ -2,7 +2,7 @@ title: "long2ip()" description: "Converts an IPv4 address from long integer to dotted string notation." sidebar: - order: 388 + order: 395 --- ## long2ip() diff --git a/docs/php/builtins/string/ltrim.md b/docs/php/builtins/string/ltrim.md index f882bec754..91e769c37b 100644 --- a/docs/php/builtins/string/ltrim.md +++ b/docs/php/builtins/string/ltrim.md @@ -2,7 +2,7 @@ title: "ltrim()" description: "Strips whitespace (or other characters) from the beginning of a string." sidebar: - order: 389 + order: 396 --- ## ltrim() diff --git a/docs/php/builtins/string/mb_strlen.md b/docs/php/builtins/string/mb_strlen.md index e8ff4fed1f..6f3778604b 100644 --- a/docs/php/builtins/string/mb_strlen.md +++ b/docs/php/builtins/string/mb_strlen.md @@ -2,20 +2,20 @@ title: "mb_strlen()" description: "Returns the character count of a string in the requested encoding." sidebar: - order: 390 + order: 397 --- ## mb_strlen() ```php -function mb_strlen(string $string, string $encoding = null): int +function mb_strlen(string $string, mixed $encoding = null): int ``` Returns the character count of a string in the requested encoding. **Parameters**: - `$string` (`string`) -- `$encoding` (`string`), default `null`, optional +- `$encoding` (`mixed`), default `null`, optional **Returns**: `int` diff --git a/docs/php/builtins/string/md5.md b/docs/php/builtins/string/md5.md index be1b4fb6a5..789e042d05 100644 --- a/docs/php/builtins/string/md5.md +++ b/docs/php/builtins/string/md5.md @@ -2,7 +2,7 @@ title: "md5()" description: "Calculates the MD5 hash of a string." sidebar: - order: 391 + order: 398 --- ## md5() diff --git a/docs/php/builtins/string/nl2br.md b/docs/php/builtins/string/nl2br.md index dc66442051..e3c47a76b4 100644 --- a/docs/php/builtins/string/nl2br.md +++ b/docs/php/builtins/string/nl2br.md @@ -2,7 +2,7 @@ title: "nl2br()" description: "Inserts HTML line breaks before newlines in a string." sidebar: - order: 392 + order: 399 --- ## nl2br() diff --git a/docs/php/builtins/string/number_format.md b/docs/php/builtins/string/number_format.md index 2cb74e012d..d10da7fa34 100644 --- a/docs/php/builtins/string/number_format.md +++ b/docs/php/builtins/string/number_format.md @@ -2,7 +2,7 @@ title: "number_format()" description: "Formats a number with grouped thousands." sidebar: - order: 393 + order: 400 --- ## number_format() diff --git a/docs/php/builtins/string/ord.md b/docs/php/builtins/string/ord.md index 886afa58c0..185fe01f55 100644 --- a/docs/php/builtins/string/ord.md +++ b/docs/php/builtins/string/ord.md @@ -2,7 +2,7 @@ title: "ord()" description: "Returns the ASCII value of the first character of a string." sidebar: - order: 394 + order: 401 --- ## ord() diff --git a/docs/php/builtins/string/printf.md b/docs/php/builtins/string/printf.md index 110d36111c..d0a8cb784f 100644 --- a/docs/php/builtins/string/printf.md +++ b/docs/php/builtins/string/printf.md @@ -2,7 +2,7 @@ title: "printf()" description: "Outputs a formatted string." sidebar: - order: 395 + order: 402 --- ## printf() diff --git a/docs/php/builtins/string/rawurldecode.md b/docs/php/builtins/string/rawurldecode.md index 7b96381168..0e11807a47 100644 --- a/docs/php/builtins/string/rawurldecode.md +++ b/docs/php/builtins/string/rawurldecode.md @@ -2,7 +2,7 @@ title: "rawurldecode()" description: "Decodes an RFC 3986 percent-encoded string without treating '+' as a space." sidebar: - order: 396 + order: 403 --- ## rawurldecode() diff --git a/docs/php/builtins/string/rawurlencode.md b/docs/php/builtins/string/rawurlencode.md index 562331a11f..d05e2bdace 100644 --- a/docs/php/builtins/string/rawurlencode.md +++ b/docs/php/builtins/string/rawurlencode.md @@ -2,7 +2,7 @@ title: "rawurlencode()" description: "URL-encodes a string using RFC 3986 percent-encoding (no '+' for spaces)." sidebar: - order: 397 + order: 404 --- ## rawurlencode() diff --git a/docs/php/builtins/string/rtrim.md b/docs/php/builtins/string/rtrim.md index 28df77644a..cd7300c8f7 100644 --- a/docs/php/builtins/string/rtrim.md +++ b/docs/php/builtins/string/rtrim.md @@ -2,7 +2,7 @@ title: "rtrim()" description: "Strips whitespace (or other characters) from the end of a string." sidebar: - order: 398 + order: 405 --- ## rtrim() diff --git a/docs/php/builtins/string/sha1.md b/docs/php/builtins/string/sha1.md index e99b597d7a..23682c7a10 100644 --- a/docs/php/builtins/string/sha1.md +++ b/docs/php/builtins/string/sha1.md @@ -2,7 +2,7 @@ title: "sha1()" description: "Calculates the SHA-1 hash of a string." sidebar: - order: 399 + order: 406 --- ## sha1() diff --git a/docs/php/builtins/string/sprintf.md b/docs/php/builtins/string/sprintf.md index a6e05241a6..aa1bf99a82 100644 --- a/docs/php/builtins/string/sprintf.md +++ b/docs/php/builtins/string/sprintf.md @@ -2,7 +2,7 @@ title: "sprintf()" description: "Returns a formatted string." sidebar: - order: 400 + order: 407 --- ## sprintf() diff --git a/docs/php/builtins/string/sscanf.md b/docs/php/builtins/string/sscanf.md index 6757fc097c..1802ecf635 100644 --- a/docs/php/builtins/string/sscanf.md +++ b/docs/php/builtins/string/sscanf.md @@ -2,7 +2,7 @@ title: "sscanf()" description: "Parses a string according to a format." sidebar: - order: 401 + order: 408 --- ## sscanf() diff --git a/docs/php/builtins/string/str_contains.md b/docs/php/builtins/string/str_contains.md index 6b104619ff..cda21557ba 100644 --- a/docs/php/builtins/string/str_contains.md +++ b/docs/php/builtins/string/str_contains.md @@ -2,7 +2,7 @@ title: "str_contains()" description: "Determines if a string contains a given substring." sidebar: - order: 402 + order: 409 --- ## str_contains() diff --git a/docs/php/builtins/string/str_ends_with.md b/docs/php/builtins/string/str_ends_with.md index 44f198915e..b395ef2ddd 100644 --- a/docs/php/builtins/string/str_ends_with.md +++ b/docs/php/builtins/string/str_ends_with.md @@ -2,7 +2,7 @@ title: "str_ends_with()" description: "Checks if a string ends with a given substring." sidebar: - order: 403 + order: 410 --- ## str_ends_with() diff --git a/docs/php/builtins/string/str_ireplace.md b/docs/php/builtins/string/str_ireplace.md index 5ddde19990..cf45bd9dc3 100644 --- a/docs/php/builtins/string/str_ireplace.md +++ b/docs/php/builtins/string/str_ireplace.md @@ -2,7 +2,7 @@ title: "str_ireplace()" description: "Case-insensitive version of str_replace()." sidebar: - order: 404 + order: 411 --- ## str_ireplace() diff --git a/docs/php/builtins/string/str_pad.md b/docs/php/builtins/string/str_pad.md index 8de2bd5b3d..bedd20609e 100644 --- a/docs/php/builtins/string/str_pad.md +++ b/docs/php/builtins/string/str_pad.md @@ -2,7 +2,7 @@ title: "str_pad()" description: "Pads a string to a certain length with another string." sidebar: - order: 405 + order: 412 --- ## str_pad() diff --git a/docs/php/builtins/string/str_repeat.md b/docs/php/builtins/string/str_repeat.md index 9e2b2be3db..97a52fa9c2 100644 --- a/docs/php/builtins/string/str_repeat.md +++ b/docs/php/builtins/string/str_repeat.md @@ -2,7 +2,7 @@ title: "str_repeat()" description: "Repeats a string a given number of times." sidebar: - order: 406 + order: 413 --- ## str_repeat() diff --git a/docs/php/builtins/string/str_replace.md b/docs/php/builtins/string/str_replace.md index 91a44425b1..a02c518519 100644 --- a/docs/php/builtins/string/str_replace.md +++ b/docs/php/builtins/string/str_replace.md @@ -2,7 +2,7 @@ title: "str_replace()" description: "Replaces all occurrences of a search string with a replacement string." sidebar: - order: 407 + order: 414 --- ## str_replace() diff --git a/docs/php/builtins/string/str_split.md b/docs/php/builtins/string/str_split.md index 4f18e9a5f7..0e599def68 100644 --- a/docs/php/builtins/string/str_split.md +++ b/docs/php/builtins/string/str_split.md @@ -2,7 +2,7 @@ title: "str_split()" description: "Converts a string into an array of chunks of the given length." sidebar: - order: 408 + order: 415 --- ## str_split() diff --git a/docs/php/builtins/string/str_starts_with.md b/docs/php/builtins/string/str_starts_with.md index d81ef8218d..57f134ae3d 100644 --- a/docs/php/builtins/string/str_starts_with.md +++ b/docs/php/builtins/string/str_starts_with.md @@ -2,7 +2,7 @@ title: "str_starts_with()" description: "Checks if a string starts with a given substring." sidebar: - order: 409 + order: 416 --- ## str_starts_with() diff --git a/docs/php/builtins/string/strcasecmp.md b/docs/php/builtins/string/strcasecmp.md index a2dda4501e..d1496bb78e 100644 --- a/docs/php/builtins/string/strcasecmp.md +++ b/docs/php/builtins/string/strcasecmp.md @@ -2,7 +2,7 @@ title: "strcasecmp()" description: "Binary safe case-insensitive string comparison. Returns negative, zero, or positive." sidebar: - order: 410 + order: 417 --- ## strcasecmp() diff --git a/docs/php/builtins/string/strcmp.md b/docs/php/builtins/string/strcmp.md index 63ba97a04c..10054ac0fd 100644 --- a/docs/php/builtins/string/strcmp.md +++ b/docs/php/builtins/string/strcmp.md @@ -2,7 +2,7 @@ title: "strcmp()" description: "Binary safe string comparison. Returns negative, zero, or positive." sidebar: - order: 411 + order: 418 --- ## strcmp() diff --git a/docs/php/builtins/string/stripslashes.md b/docs/php/builtins/string/stripslashes.md index c7d1739ef2..1c2ee636d9 100644 --- a/docs/php/builtins/string/stripslashes.md +++ b/docs/php/builtins/string/stripslashes.md @@ -2,7 +2,7 @@ title: "stripslashes()" description: "Removes backslashes from a string previously escaped by addslashes." sidebar: - order: 412 + order: 419 --- ## stripslashes() diff --git a/docs/php/builtins/string/strlen.md b/docs/php/builtins/string/strlen.md index c2e509b625..ed688ef8eb 100644 --- a/docs/php/builtins/string/strlen.md +++ b/docs/php/builtins/string/strlen.md @@ -2,7 +2,7 @@ title: "strlen()" description: "Returns the length of a string." sidebar: - order: 413 + order: 420 --- ## strlen() diff --git a/docs/php/builtins/string/strpos.md b/docs/php/builtins/string/strpos.md index 25c4a7182a..081f3ad250 100644 --- a/docs/php/builtins/string/strpos.md +++ b/docs/php/builtins/string/strpos.md @@ -2,7 +2,7 @@ title: "strpos()" description: "Finds the numeric position of the first occurrence of a substring." sidebar: - order: 414 + order: 421 --- ## strpos() diff --git a/docs/php/builtins/string/strrev.md b/docs/php/builtins/string/strrev.md index 32b3e16bc4..563ec20c76 100644 --- a/docs/php/builtins/string/strrev.md +++ b/docs/php/builtins/string/strrev.md @@ -2,7 +2,7 @@ title: "strrev()" description: "Reverses a string." sidebar: - order: 415 + order: 422 --- ## strrev() diff --git a/docs/php/builtins/string/strrpos.md b/docs/php/builtins/string/strrpos.md index 1d20249571..64fa0c6284 100644 --- a/docs/php/builtins/string/strrpos.md +++ b/docs/php/builtins/string/strrpos.md @@ -2,7 +2,7 @@ title: "strrpos()" description: "Finds the numeric position of the last occurrence of a substring." sidebar: - order: 416 + order: 423 --- ## strrpos() diff --git a/docs/php/builtins/string/strstr.md b/docs/php/builtins/string/strstr.md index a1ba3575b0..554554d40f 100644 --- a/docs/php/builtins/string/strstr.md +++ b/docs/php/builtins/string/strstr.md @@ -2,7 +2,7 @@ title: "strstr()" description: "Returns the portion of a string starting at the first occurrence of a substring." sidebar: - order: 417 + order: 424 --- ## strstr() diff --git a/docs/php/builtins/string/strtolower.md b/docs/php/builtins/string/strtolower.md index 3fe2903725..84803c2177 100644 --- a/docs/php/builtins/string/strtolower.md +++ b/docs/php/builtins/string/strtolower.md @@ -2,7 +2,7 @@ title: "strtolower()" description: "Converts a string to lowercase." sidebar: - order: 418 + order: 425 --- ## strtolower() diff --git a/docs/php/builtins/string/strtoupper.md b/docs/php/builtins/string/strtoupper.md index 4c5d25d573..edae29fbd9 100644 --- a/docs/php/builtins/string/strtoupper.md +++ b/docs/php/builtins/string/strtoupper.md @@ -2,7 +2,7 @@ title: "strtoupper()" description: "Converts a string to uppercase." sidebar: - order: 419 + order: 426 --- ## strtoupper() diff --git a/docs/php/builtins/string/substr.md b/docs/php/builtins/string/substr.md index a9e2a09f33..9a3c841661 100644 --- a/docs/php/builtins/string/substr.md +++ b/docs/php/builtins/string/substr.md @@ -2,7 +2,7 @@ title: "substr()" description: "Returns a portion of a string specified by the offset and length." sidebar: - order: 420 + order: 427 --- ## substr() diff --git a/docs/php/builtins/string/substr_replace.md b/docs/php/builtins/string/substr_replace.md index bcdc21e217..22eb1b9bd0 100644 --- a/docs/php/builtins/string/substr_replace.md +++ b/docs/php/builtins/string/substr_replace.md @@ -2,7 +2,7 @@ title: "substr_replace()" description: "Replaces text within a portion of a string." sidebar: - order: 421 + order: 428 --- ## substr_replace() diff --git a/docs/php/builtins/string/trim.md b/docs/php/builtins/string/trim.md index 32ce4fdf5c..cf0edb645a 100644 --- a/docs/php/builtins/string/trim.md +++ b/docs/php/builtins/string/trim.md @@ -2,7 +2,7 @@ title: "trim()" description: "Strips whitespace (or other characters) from the beginning and end of a string." sidebar: - order: 422 + order: 429 --- ## trim() diff --git a/docs/php/builtins/string/ucfirst.md b/docs/php/builtins/string/ucfirst.md index fb0e48cf26..c649c2807f 100644 --- a/docs/php/builtins/string/ucfirst.md +++ b/docs/php/builtins/string/ucfirst.md @@ -2,7 +2,7 @@ title: "ucfirst()" description: "Uppercases the first character of a string." sidebar: - order: 423 + order: 430 --- ## ucfirst() diff --git a/docs/php/builtins/string/ucwords.md b/docs/php/builtins/string/ucwords.md index d20540bd5c..3b9cd3b229 100644 --- a/docs/php/builtins/string/ucwords.md +++ b/docs/php/builtins/string/ucwords.md @@ -2,7 +2,7 @@ title: "ucwords()" description: "Uppercases the first character of each word in a string." sidebar: - order: 424 + order: 431 --- ## ucwords() diff --git a/docs/php/builtins/string/urldecode.md b/docs/php/builtins/string/urldecode.md index 1a4d9aa9fa..6a393dbe46 100644 --- a/docs/php/builtins/string/urldecode.md +++ b/docs/php/builtins/string/urldecode.md @@ -2,7 +2,7 @@ title: "urldecode()" description: "Decodes a URL-encoded string, including '+' as a space." sidebar: - order: 425 + order: 432 --- ## urldecode() diff --git a/docs/php/builtins/string/urlencode.md b/docs/php/builtins/string/urlencode.md index 5844107a29..3fcd3531a8 100644 --- a/docs/php/builtins/string/urlencode.md +++ b/docs/php/builtins/string/urlencode.md @@ -2,7 +2,7 @@ title: "urlencode()" description: "URL-encodes a string using application/x-www-form-urlencoded rules." sidebar: - order: 426 + order: 433 --- ## urlencode() diff --git a/docs/php/builtins/string/vprintf.md b/docs/php/builtins/string/vprintf.md index 5e1f47a5cb..ec24861dc8 100644 --- a/docs/php/builtins/string/vprintf.md +++ b/docs/php/builtins/string/vprintf.md @@ -2,7 +2,7 @@ title: "vprintf()" description: "Outputs a formatted string using an array of values." sidebar: - order: 427 + order: 434 --- ## vprintf() diff --git a/docs/php/builtins/string/vsprintf.md b/docs/php/builtins/string/vsprintf.md index d1d0772d7b..a4303072fd 100644 --- a/docs/php/builtins/string/vsprintf.md +++ b/docs/php/builtins/string/vsprintf.md @@ -2,7 +2,7 @@ title: "vsprintf()" description: "Returns a formatted string using an array of values." sidebar: - order: 428 + order: 435 --- ## vsprintf() diff --git a/docs/php/builtins/string/wordwrap.md b/docs/php/builtins/string/wordwrap.md index 2ee072ef29..f38066cc4a 100644 --- a/docs/php/builtins/string/wordwrap.md +++ b/docs/php/builtins/string/wordwrap.md @@ -2,7 +2,7 @@ title: "wordwrap()" description: "Wraps a string to a given number of characters." sidebar: - order: 429 + order: 436 --- ## wordwrap() diff --git a/docs/php/builtins/type.md b/docs/php/builtins/type.md index a14755a382..b334e2dcf2 100644 --- a/docs/php/builtins/type.md +++ b/docs/php/builtins/type.md @@ -35,5 +35,5 @@ sidebar: | [`is_resource()`](./type/is_resource.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | | [`is_scalar()`](./type/is_scalar.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | | [`is_string()`](./type/is_string.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | -| [`settype()`](./type/settype.md) | `(mixed $var, string $type): bool` | `bool` | ✓ | ✓ | +| [`settype()`](./type/settype.md) | `(mixed &$var, string $type): bool` | `bool` | ✓ | ✓ | | [`strval()`](./type/strval.md) | `(mixed $value): string` | `string` | ✓ | ✓ | diff --git a/docs/php/builtins/type/boolval.md b/docs/php/builtins/type/boolval.md index cf82ba0986..1abcbd1556 100644 --- a/docs/php/builtins/type/boolval.md +++ b/docs/php/builtins/type/boolval.md @@ -2,7 +2,7 @@ title: "boolval()" description: "Returns the boolean value of a variable." sidebar: - order: 430 + order: 437 --- ## boolval() diff --git a/docs/php/builtins/type/ctype_alnum.md b/docs/php/builtins/type/ctype_alnum.md index 2a4885e7b1..b4a72a3ff7 100644 --- a/docs/php/builtins/type/ctype_alnum.md +++ b/docs/php/builtins/type/ctype_alnum.md @@ -2,7 +2,7 @@ title: "ctype_alnum()" description: "Checks if all characters in the string are alphanumeric." sidebar: - order: 431 + order: 438 --- ## ctype_alnum() diff --git a/docs/php/builtins/type/ctype_alpha.md b/docs/php/builtins/type/ctype_alpha.md index b502165c44..3af03fcc4e 100644 --- a/docs/php/builtins/type/ctype_alpha.md +++ b/docs/php/builtins/type/ctype_alpha.md @@ -2,7 +2,7 @@ title: "ctype_alpha()" description: "Checks if all characters in the string are alphabetic." sidebar: - order: 432 + order: 439 --- ## ctype_alpha() diff --git a/docs/php/builtins/type/ctype_digit.md b/docs/php/builtins/type/ctype_digit.md index 249ce81014..4582c2dffe 100644 --- a/docs/php/builtins/type/ctype_digit.md +++ b/docs/php/builtins/type/ctype_digit.md @@ -2,7 +2,7 @@ title: "ctype_digit()" description: "Checks if all characters in the string are digits." sidebar: - order: 433 + order: 440 --- ## ctype_digit() diff --git a/docs/php/builtins/type/ctype_space.md b/docs/php/builtins/type/ctype_space.md index 55526db2d3..4e78825323 100644 --- a/docs/php/builtins/type/ctype_space.md +++ b/docs/php/builtins/type/ctype_space.md @@ -2,7 +2,7 @@ title: "ctype_space()" description: "Checks if all characters in the string are whitespace characters." sidebar: - order: 434 + order: 441 --- ## ctype_space() diff --git a/docs/php/builtins/type/floatval.md b/docs/php/builtins/type/floatval.md index a97c030c73..89eac6a018 100644 --- a/docs/php/builtins/type/floatval.md +++ b/docs/php/builtins/type/floatval.md @@ -2,7 +2,7 @@ title: "floatval()" description: "Returns the float value of a variable." sidebar: - order: 435 + order: 442 --- ## floatval() diff --git a/docs/php/builtins/type/get_resource_id.md b/docs/php/builtins/type/get_resource_id.md index 1255472bfd..93d7eaa2b9 100644 --- a/docs/php/builtins/type/get_resource_id.md +++ b/docs/php/builtins/type/get_resource_id.md @@ -2,7 +2,7 @@ title: "get_resource_id()" description: "Returns an integer identifier for the given resource." sidebar: - order: 436 + order: 443 --- ## get_resource_id() diff --git a/docs/php/builtins/type/get_resource_type.md b/docs/php/builtins/type/get_resource_type.md index a980e0581a..bfc1214b66 100644 --- a/docs/php/builtins/type/get_resource_type.md +++ b/docs/php/builtins/type/get_resource_type.md @@ -2,7 +2,7 @@ title: "get_resource_type()" description: "Returns the type of a resource." sidebar: - order: 437 + order: 444 --- ## get_resource_type() diff --git a/docs/php/builtins/type/gettype.md b/docs/php/builtins/type/gettype.md index 11ee198196..93cb4b7f35 100644 --- a/docs/php/builtins/type/gettype.md +++ b/docs/php/builtins/type/gettype.md @@ -2,7 +2,7 @@ title: "gettype()" description: "Returns the type of a variable as a string." sidebar: - order: 438 + order: 445 --- ## gettype() diff --git a/docs/php/builtins/type/intval.md b/docs/php/builtins/type/intval.md index 79587ab19f..216ce10ff6 100644 --- a/docs/php/builtins/type/intval.md +++ b/docs/php/builtins/type/intval.md @@ -2,7 +2,7 @@ title: "intval()" description: "Returns the integer value of a variable." sidebar: - order: 439 + order: 446 --- ## intval() diff --git a/docs/php/builtins/type/is_array.md b/docs/php/builtins/type/is_array.md index ac5dac476e..c246c77bbf 100644 --- a/docs/php/builtins/type/is_array.md +++ b/docs/php/builtins/type/is_array.md @@ -2,7 +2,7 @@ title: "is_array()" description: "Checks whether a variable is an array." sidebar: - order: 440 + order: 447 --- ## is_array() diff --git a/docs/php/builtins/type/is_bool.md b/docs/php/builtins/type/is_bool.md index 0d1672d059..3b44dd2ca9 100644 --- a/docs/php/builtins/type/is_bool.md +++ b/docs/php/builtins/type/is_bool.md @@ -2,7 +2,7 @@ title: "is_bool()" description: "Checks whether a variable is a boolean." sidebar: - order: 441 + order: 448 --- ## is_bool() diff --git a/docs/php/builtins/type/is_callable.md b/docs/php/builtins/type/is_callable.md index 04116551bd..56c37bf074 100644 --- a/docs/php/builtins/type/is_callable.md +++ b/docs/php/builtins/type/is_callable.md @@ -2,7 +2,7 @@ title: "is_callable()" description: "Checks whether a variable can be called as a function." sidebar: - order: 442 + order: 449 --- ## is_callable() diff --git a/docs/php/builtins/type/is_double.md b/docs/php/builtins/type/is_double.md index da20cb790e..14a51ebe9a 100644 --- a/docs/php/builtins/type/is_double.md +++ b/docs/php/builtins/type/is_double.md @@ -2,7 +2,7 @@ title: "is_double()" description: "Alias of is_float()." sidebar: - order: 443 + order: 450 --- ## is_double() diff --git a/docs/php/builtins/type/is_float.md b/docs/php/builtins/type/is_float.md index 7b6dab8a9a..a4a5b0efb9 100644 --- a/docs/php/builtins/type/is_float.md +++ b/docs/php/builtins/type/is_float.md @@ -2,7 +2,7 @@ title: "is_float()" description: "Checks whether a variable is a floating-point number." sidebar: - order: 444 + order: 451 --- ## is_float() diff --git a/docs/php/builtins/type/is_int.md b/docs/php/builtins/type/is_int.md index bd42b91932..2a19122906 100644 --- a/docs/php/builtins/type/is_int.md +++ b/docs/php/builtins/type/is_int.md @@ -2,7 +2,7 @@ title: "is_int()" description: "Checks whether a variable is an integer." sidebar: - order: 445 + order: 452 --- ## is_int() diff --git a/docs/php/builtins/type/is_integer.md b/docs/php/builtins/type/is_integer.md index bfd218e834..222220d814 100644 --- a/docs/php/builtins/type/is_integer.md +++ b/docs/php/builtins/type/is_integer.md @@ -2,7 +2,7 @@ title: "is_integer()" description: "Alias of is_int()." sidebar: - order: 446 + order: 453 --- ## is_integer() diff --git a/docs/php/builtins/type/is_iterable.md b/docs/php/builtins/type/is_iterable.md index 749806f076..a4d31b32b1 100644 --- a/docs/php/builtins/type/is_iterable.md +++ b/docs/php/builtins/type/is_iterable.md @@ -2,7 +2,7 @@ title: "is_iterable()" description: "Checks whether a variable is iterable." sidebar: - order: 447 + order: 454 --- ## is_iterable() diff --git a/docs/php/builtins/type/is_long.md b/docs/php/builtins/type/is_long.md index 7a57ac2db8..a7a23ba877 100644 --- a/docs/php/builtins/type/is_long.md +++ b/docs/php/builtins/type/is_long.md @@ -2,7 +2,7 @@ title: "is_long()" description: "Alias of is_int()." sidebar: - order: 448 + order: 455 --- ## is_long() diff --git a/docs/php/builtins/type/is_null.md b/docs/php/builtins/type/is_null.md index bed21b20c5..c7b59ece99 100644 --- a/docs/php/builtins/type/is_null.md +++ b/docs/php/builtins/type/is_null.md @@ -2,7 +2,7 @@ title: "is_null()" description: "Checks whether a variable is null." sidebar: - order: 449 + order: 456 --- ## is_null() diff --git a/docs/php/builtins/type/is_numeric.md b/docs/php/builtins/type/is_numeric.md index 1dce7eed3c..e8532a57a1 100644 --- a/docs/php/builtins/type/is_numeric.md +++ b/docs/php/builtins/type/is_numeric.md @@ -2,7 +2,7 @@ title: "is_numeric()" description: "Checks whether a variable is a number or a numeric string." sidebar: - order: 450 + order: 457 --- ## is_numeric() diff --git a/docs/php/builtins/type/is_object.md b/docs/php/builtins/type/is_object.md index d4cc1fa855..61675bc885 100644 --- a/docs/php/builtins/type/is_object.md +++ b/docs/php/builtins/type/is_object.md @@ -2,7 +2,7 @@ title: "is_object()" description: "Checks whether a variable is an object." sidebar: - order: 451 + order: 458 --- ## is_object() diff --git a/docs/php/builtins/type/is_real.md b/docs/php/builtins/type/is_real.md index 6797b0e75d..5b978bd894 100644 --- a/docs/php/builtins/type/is_real.md +++ b/docs/php/builtins/type/is_real.md @@ -2,7 +2,7 @@ title: "is_real()" description: "Alias of is_float()." sidebar: - order: 452 + order: 459 --- ## is_real() diff --git a/docs/php/builtins/type/is_resource.md b/docs/php/builtins/type/is_resource.md index ebbaaa9528..e424c0e9a6 100644 --- a/docs/php/builtins/type/is_resource.md +++ b/docs/php/builtins/type/is_resource.md @@ -2,7 +2,7 @@ title: "is_resource()" description: "Checks whether a variable is a resource." sidebar: - order: 453 + order: 460 --- ## is_resource() diff --git a/docs/php/builtins/type/is_scalar.md b/docs/php/builtins/type/is_scalar.md index b24d4dafe9..86af209373 100644 --- a/docs/php/builtins/type/is_scalar.md +++ b/docs/php/builtins/type/is_scalar.md @@ -2,7 +2,7 @@ title: "is_scalar()" description: "Checks whether a variable is a scalar." sidebar: - order: 454 + order: 461 --- ## is_scalar() diff --git a/docs/php/builtins/type/is_string.md b/docs/php/builtins/type/is_string.md index 5959f1bfc2..8e91b1f270 100644 --- a/docs/php/builtins/type/is_string.md +++ b/docs/php/builtins/type/is_string.md @@ -2,7 +2,7 @@ title: "is_string()" description: "Checks whether a variable is a string." sidebar: - order: 455 + order: 462 --- ## is_string() diff --git a/docs/php/builtins/type/settype.md b/docs/php/builtins/type/settype.md index c6d83b4073..48d31cc32f 100644 --- a/docs/php/builtins/type/settype.md +++ b/docs/php/builtins/type/settype.md @@ -2,13 +2,13 @@ title: "settype()" description: "Sets the type of a variable." sidebar: - order: 456 + order: 463 --- ## settype() ```php -function settype(mixed $var, string $type): bool +function settype(mixed &$var, string $type): bool ``` Sets the type of a variable. diff --git a/docs/php/builtins/type/strval.md b/docs/php/builtins/type/strval.md index ac98c6c272..8be5f573f5 100644 --- a/docs/php/builtins/type/strval.md +++ b/docs/php/builtins/type/strval.md @@ -2,7 +2,7 @@ title: "strval()" description: "Gets the string value of a variable." sidebar: - order: 457 + order: 464 --- ## strval() diff --git a/docs/php/eval.md b/docs/php/eval.md index ac677653cb..a7c1396ce8 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -41,8 +41,11 @@ bridge into the standalone binary; programs without that requirement do not. `--with-eval` force-links the bridge for testing or indirect use, but it is not required to enable the language construct. -The AOT and fallback paths are covered on macOS ARM64, Linux ARM64, and Linux -x86_64. CI runs dedicated eval integration shards for every supported target. +The AOT and fallback paths are covered on macOS ARM64, Linux ARM64, Linux +x86_64, and the experimental Windows x86_64 backend. Windows eval fixtures run +directly on native Windows hosts as part of the strict 8-shard codegen gate. +Windows remains experimental until the first complete native run is green and +the target-policy review confirms that no Windows-only reduced semantics remain. ## Performance diff --git a/docs/php/streams.md b/docs/php/streams.md index 40eb9cd356..b2895e4107 100644 --- a/docs/php/streams.md +++ b/docs/php/streams.md @@ -94,7 +94,10 @@ after calling the `setZipPassword(string $password)` compiler extension on a 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 +confidentiality mechanism. Its per-entry encryption headers use operating-system +entropy; on Windows the bridge obtains those bytes from `BCryptGenRandom` and +fails the archive write if the system provider is unavailable. `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 @@ -270,9 +273,9 @@ type, or as `mixed`, when returning associative stat arrays with string keys. | Function | Signature | Description | |---|---|---| -| `stream_get_transports()` | `stream_get_transports(): array` | Return recognized socket transports: `tcp`, `udp`, `unix`, `udg`, `tls`, `ssl`, `sslv2`, `sslv3`, `tlsv1.0`, `tlsv1.1`, `tlsv1.2`, and `tlsv1.3`. TLS-version names all use rustls default negotiation. | -| `stream_socket_server()` | `stream_socket_server($address): resource\|false` | Bind a server socket for `[tcp://]host:port`, `udp://host:port`, `unix:///path`, or `udg:///path`. TCP and Unix-stream sockets listen; UDP and Unix-datagram sockets only bind. | -| `stream_socket_client()` | `stream_socket_client($address): resource\|false` | Open a client stream for `[tcp://]host:port`, `udp://host:port`, `unix:///path`, or `udg:///path`. | +| `stream_get_transports()` | `stream_get_transports(): array` | Return recognized socket transports: `tcp`, `udp`, `tls`, `ssl`, `sslv2`, `sslv3`, `tlsv1.0`, `tlsv1.1`, `tlsv1.2`, and `tlsv1.3`, plus `unix` and `udg` on non-Windows targets. TLS-version names all use rustls default negotiation. | +| `stream_socket_server()` | `stream_socket_server($address): resource\|false` | Bind a server socket for `[tcp://]host:port` or `udp://host:port`, and on non-Windows targets `unix:///path` or `udg:///path`. TCP and Unix-stream sockets listen; UDP and Unix-datagram sockets only bind. PHP does not register Unix-domain transports on Windows, so those addresses return `false` there. | +| `stream_socket_client()` | `stream_socket_client($address): resource\|false` | Open a client stream for `[tcp://]host:port` or `udp://host:port`, and on non-Windows targets `unix:///path` or `udg:///path`. PHP does not register Unix-domain transports on Windows, so those addresses return `false` there. | | `stream_socket_accept()` | `stream_socket_accept($socket): resource\|false` | Accept the next pending connection from a listening stream. | | `stream_socket_enable_crypto()` | `stream_socket_enable_crypto(resource $stream, bool $enable, int $crypto_method = null, resource $session_stream = null): bool` | Attach TLS to an already-connected TCP fd. `$enable=false` unwinds the session (sends `close_notify` and clears the per-fd TLS handle), leaving the fd a plain TCP socket, then reports `true`; it is a no-op when no session is attached. | | `fsockopen()` | `fsockopen(string $hostname, int $port, int &$error_code = null, string &$error_message = null, float $timeout = null): resource\|false` | Open a TCP connection to `$hostname:$port`, writing optional by-reference error outputs. The timeout arg is evaluated but the OS default connect timeout is used in v1. | @@ -288,8 +291,10 @@ type, or as `mixed`, when returning associative stat arrays with string keys. | `popen()` | `popen(string $command, string $mode): resource\|false` | Open a pipe to a process in read (`"r"`) or write (`"w"`) mode. | | `pclose()` | `pclose($handle): int` | Close a process pipe and return its termination status. | -Socket addresses use `[tcp://]host:port`, `udp://host:port`, `unix:///path`, or -`udg:///path`. Host names are resolved through the system resolver to IPv4. +Socket addresses use `[tcp://]host:port` or `udp://host:port`. Non-Windows +targets also support `unix:///path` and `udg:///path`; matching php-src, +Windows neither reports nor accepts these Unix-domain transports. Host names +are resolved through the system resolver to IPv4. ## Directory streams diff --git a/docs/php/strings.md b/docs/php/strings.md index 37dbaa6a8f..52784733a5 100644 --- a/docs/php/strings.md +++ b/docs/php/strings.md @@ -173,6 +173,8 @@ Read-only. Negative indices count from end. Out-of-bounds returns empty string. | `sscanf()` | `sscanf($str, $fmt): array` | Parse string with format (%d, %f, %s, %%). Matched fields are returned as substrings (e.g. `%f` yields `"3.14"`), mirroring the existing `%d` behavior. | | `addslashes()` | `addslashes($str): string` | Escape quotes and backslashes | | `stripslashes()` | `stripslashes($str): string` | Remove escape backslashes | +| `escapeshellarg()` | `escapeshellarg($arg): string` | Quote one argument for safe shell use. Uses POSIX single quotes on macOS/Linux and PHP's Windows command-line quoting rule on Windows; NUL bytes throw `ValueError`. Weak scalar strings are accepted only when their type is statically known to exclude arrays and objects. | +| `escapeshellcmd()` | `escapeshellcmd($command): string` | Escape shell metacharacters in a command. Uses POSIX backslashes on macOS/Linux and cmd.exe carets on Windows; NUL bytes throw `ValueError`. Weak scalar strings are accepted only when their type is statically known to exclude arrays and objects. | | `nl2br()` | `nl2br($str): string` | Insert `
` before newlines | | `wordwrap()` | `wordwrap($str [, $width [, $break [, $cut]]]): string` | Wrap text at word boundaries; set `$cut` to break over-long words | | `bin2hex()` | `bin2hex($str): string` | Convert binary to hex | diff --git a/docs/specs/mixed-string-ownership-debts.md b/docs/specs/mixed-string-ownership-debts.md new file mode 100644 index 0000000000..d7442823dd --- /dev/null +++ b/docs/specs/mixed-string-ownership-debts.md @@ -0,0 +1,256 @@ +--- +title: "Mixed/String Ownership Debts" +description: "Findings on the heap blocks stranded by string materialization: what was fixed, what remains, and why the remaining fix is not uniform." +sidebar: + order: 13 +--- + +This document records an investigation into heap blocks that are allocated while +materializing PHP strings and then never reclaimed. Two of the paths are fixed; the +largest one is diagnosed but deliberately left open, because the obvious fix is a +use-after-free rather than a leak fix. + +Every claim below is tagged **[measured]** when it was reproduced by running a compiled +program, or **[read]** when it comes only from reading the source. That distinction is +load-bearing: several confident-sounding conclusions in this area turned out to be wrong +when executed, and they are called out in [Pitfalls](#pitfalls-for-whoever-picks-this-up). + +All measurements were taken on macOS aarch64 with `--heap-debug`, against +`php -d xdebug.mode=off` (PHP 8.5.6) as the oracle. + +## Background: two allocators, one pointer + +Several runtime helpers return *either* a borrowed slice of the 64 KiB concat arena +(`_concat_buf`) *or* an owned `__rt_heap_alloc` block, decided at run time by whether the +data outgrew the arena. The caller cannot know at compile time which it received. + +This is safe to reclaim unconditionally because `__rt_heap_free_safe` separates the two +shapes *exactly* rather than heuristically: `_concat_buf` and `_heap_buf` are distinct +`.comm` objects (`src/codegen_support/runtime/data/fixed.rs`), so an arena pointer can +never satisfy the managed-heap window test and passes through untouched. + +Consequently every fix in this area needs an **arena-sized negative control** alongside +the oversized fixture: freeing a borrowed arena slice would be a wild free, which no +leak-only assertion would catch. + +## Fixed + +### `stream_get_contents()` boxed into a Mixed cell + +`stream_get_contents()` is typed `string|false`, so its result is boxed into a Mixed cell +inside the lowering itself. The boxing **copies rather than adopts**: +`__rt_mixed_from_value` tag 1 goes through `__rt_str_persist`, which always allocates and +byte-copies. The slurped block was therefore left with no owner — and, because no EIR +value ever names that pointer, with no `release` either. + +**[measured]** A 500 KB stream stranded one 524288-byte block per read: `live_blocks=4 +live_bytes=2097216` over four reads, versus `clean` after the fix. + +Fixed by `box_read_all_result_and_release_source` in +`src/codegen/lower_inst/builtins/io.rs`, called at both read-all lowering sites. + +The release is correct here **only because no EIR value names the pre-box pointer**. It +must not be copied to a builtin typed plain `string`: `fread()` is exactly that shape, and +there the EIR release already reclaims the block, so a second free would be a double free. + +### The six `file_get_contents()` URL branches + +Same shape one level down. Each branch (http / https / ftp, on both architectures) slurps +the response with `__rt_stream_get_contents`, persists it with `__rt_str_persist` for +ownership, and then drops the slurped pointer. + +**[measured]** A 200 KB body cost one block of roughly the doubled accumulation capacity +per read (`live_blocks=1 live_bytes=262076` for a single read, linear in call count). + +Fixed by `emit_release_slurped_source` in +`src/codegen_support/runtime/io/file_get_contents_url.rs`, applied at all six sites. The +persisted copy is parked on the temporary stack rather than in a frame slot, so the helper +stays independent of each branch's differing frame layout. + +## Open: the `Mixed` → `Str` cast leak + +This is the large one. It affects a family of builtins, not a single function. + +**[measured]** Passing a Mixed/union-typed value where a builtin declares a `string` +parameter strands **one heap block per call, sized to the whole source string** — not to +the result. A 4095-byte source leaks a 4112-byte block per call, strictly linear in call +count. Program output stays correct throughout, so only heap accounting reveals it. + +### Correcting the obvious wrong turn first + +**[measured]** `Op::MixedCastString` is **dead code**: it has zero construction sites in the +tree. Grepping the EIR for it finds nothing and invites the conclusion that this path does +not exist. The instruction actually emitted is `Op::Cast` with +`Immediate::CastTarget(IrType::Str)`, produced at `src/ir_lower/expr/mod.rs:14742`, and the +EIR prints it as `cast vN Str`. Several ownership allow-lists name the dead variant +instead of the live one, including `src/codegen/lower_inst/ownership.rs`; they are inert. + +**[read]** The `alloc_concat` effect on `Op::Cast` is not a blanket lie either. Only the +tag-1 (already-a-string) arm heap-allocates, via `__rt_str_persist` +(`src/codegen_support/runtime/arrays/mixed_cast_string.rs`, both architectures). Tags 0/2/3 +go through `__rt_itoa` / `__rt_ftoa`, which do write the arena. **[read]** The helper has +no object arm at all — `__toString` is dispatched at the call site +(`src/codegen/lower_inst/conversions.rs`) before the helper is reached. + +**[read]** The `ALLOC_CONCAT` / `ALLOC_HEAP` effect bits are read by nothing. The only +predicate that inspects them puts both in the same set and is never called. Correcting the +annotation alone would change no behaviour. + +### The decision site + +There is **no liveness or dataflow pass**. Releases are emitted eagerly at lowering time by +a syntactic producer classifier (`LoweringContext::value_is_owning_temporary`, +`src/ir_lower/context.rs`). That classifier *does* recognise the cast value as owning. One +specific consumer then decides to skip its release: + +`src/ir_lower/expr/mod.rs:13413-13431` — the `continue` at **line 13427** drops the release +when both of: + +1. `return_alias.may_alias_parameter(idx)` — true whenever `result_ownership` is + `MayAliasArguments`, which is the *default* (`src/ir/runtime_fn.rs`), mapping to + `ReturnArgAlias::Unknown`, which answers true for every index; and +2. `call_result_may_alias_arg(...)` — true when the resolved result type is + `Str`/`Mixed`/`Union` (`src/ir_lower/context.rs`). + +**[measured]** The same producer leaks or not depending purely on the *consumer*. In +`$s = (string)$m;` the cast value is an assignment source, so it goes through `store_local` +(`src/ir_lower/context.rs`), which has no alias guard at all — there the release *is* +emitted and nothing leaks. In `substr($m, -3)` it is a call argument, so it hits the guard. + +### Why a uniform fix is a use-after-free + +The guard is **legitimate** for part of the family. Those builtins return an *interior +pointer into the cast buffer*: + +- **[read]** `substr` computes `add x1, x1, x0` (`src/codegen/lower_inst/builtins/strings.rs`, + x86 counterpart nearby) — the result points inside the source. +- **[read]** `trim` returns "the adjusted borrowed string slice … No heap allocation" + (`src/codegen_support/runtime/strings/trim.rs`). +- **[read]** `strstr` is the same shape. + +Releasing the cast copy right after the call would destroy the storage the result points +at. For these, closing the leak requires **extending the lifetime** — releasing after the +result has been consumed or persisted — not adding a release. + +The rest are **false positives** of the guard: their result is freshly built, so releasing +the source is safe. They are classified `MayAliasArguments` only because they were never +added to the `Fresh` list. **[read]** `ucfirst` builds through `__rt_strcopy` into the +arena; `md5`, `str_replace`, `str_pad` likewise produce fresh output. + +**[measured]** Builtins that do *not* leak, and why: `strtoupper` / `strrev` route through +`UnaryString` ⇒ `Fresh`; `strpos` / `explode` are already in the `Fresh` list; `strlen` / +`str_contains` have non-heap results so the second conjunct is false. + +**[measured]** A three-way discriminator isolates both conjuncts cleanly: +`htmlspecialchars($m)` (`Independent` ⇒ no alias) is clean; `str_pad($m, 4096)` +(`MayAliasArguments`, Str result) leaks; `strcasecmp($m, "zz")` (`MayAliasArguments`, Int +result) is clean. + +### Suggested approach + +Correcting the aliasing classification per builtin is the tractable half: builtins whose +result provably does not alias their string argument can be moved out of the default +`MayAliasArguments`, which makes the existing release fire with no lifetime change. Two +cautions: `result_ownership` is read by other passes, so the change is not purely local; +and each entry needs its result-aliasing proven from the lowering, not assumed. + +The genuinely-aliasing half (`substr`, `trim`, `strstr`, …) needs a real lifetime +extension and should be treated as separate work. + +## Other defects found during this investigation + +None of these are fixed. All were found while auditing the paths above. + +### `phar://` dynamic reads corrupt data + +**[measured]** This is data corruption, not just a leak. `__rt_phar_read_entry` +(`src/codegen_support/runtime/io/phar_read.rs`, both architectures) returns the +`__rt_stream_get_contents` pointer **verbatim, without `__rt_str_persist`**. When the entry +fits the arena, that is a `_concat_buf` slice, and `box_owned_string_or_false_result` +(`src/codegen/lower_inst/builtins/io.rs`) writes it into the Mixed cell as if it were +owned. Subsequent concat traffic overwrites it in place. + +Reading a 100-byte phar entry, running 200 concatenations, then printing it yields +`xxxxxxxxxx` instead of the content. Only the **dynamic** `phar://` route is affected; the +literal route goes through `lower_literal_phar_file_get_contents` and is correct. This is +the only branch of `__rt_file_get_contents_maybe_url` that does not honour the "owned heap +string" contract every other branch honours. + +### `hash_file()` on a phar entry leaks, and can exhaust the heap + +**[measured]** `lower_hash_file_*` passes the bytes to `__rt_hash` and never releases them: +two blocks per call. With a 2 MB entry the second call dies with `Fatal error: heap memory +exhausted`, where PHP completes. + +**[read]** One of those two blocks comes from a second defect: the assembly fallback in +`__rt_phar_read_entry` reads the entire archive and its epilogue returns without releasing +it. That fallback is only taken because `publish_dynamic_phar_function_pointers` is called +solely from `lower_file_get_contents`, never from `lower_hash_file_*`. + +### Bounded `stream_get_contents($f, N)` still overruns the arena + +**[measured]** The `lseek`-based bound that fixed `fread()` was never applied to the bounded +arm. It still copies into `_concat_buf + start + total` with `total` bounded only by the +caller's requested length, against a fixed 64 KiB `.comm`. Reading with a large `N` +overruns the arena and clobbers the runtime's stream table, surfacing as +`Fatal error: Uncaught TypeError: fclose(): Argument #1 ($stream) must be of type resource, +unknown given`. The read-all arm has both a fix and a regression test; the bounded arm has +neither. + +### Indexing a `stream_get_contents()` result returns empty + +**[measured]** `$body[0]` yields an empty string while `$body === $expected` and +`strlen($body)` are both correct on the same value. Reproduced on the compiler *before* the +fixes in this document, so it is pre-existing and unrelated to them. + +### Deep `===` on a Mixed plus a later `unset()` leaks per iteration + +**[measured]** Isolated with a four-way matrix: a deep `===` comparison alone is clean, an +`unset()` alone is clean, and the two together leak one block per loop iteration. Program +output stays correct in all four cells. + +### Large top-level strings are never reclaimed at exit + +**[measured]** A top-level `Str` local holding a heap block (e.g. `str_repeat("Q", 500000)`) +survives to process exit, and `unset()` does not release it. This is mostly a nuisance for +test authoring — it pollutes `--heap-debug` totals — but it is worth knowing. + +## Pitfalls for whoever picks this up + +These cost real time during this investigation. + +- **Byte totals cannot discriminate which block leaked.** `__rt_str_persist` rounds to the + next power of two, and the accumulation buffer doubles from 65536. For any leaking case + both are the *same size*, so "the arithmetic proves it was buffer X" is never a valid + argument. Count blocks and vary one factor at a time instead. +- **Discard mode gives false zeroes.** Measuring a builtin whose result is unused can + report no leak purely because the call was dead-code-eliminated. Consume the result. +- **A length-only assertion cannot detect free-before-copy.** Releasing the source before + the copy completes still yields the right length. Fixtures must read the bytes; the ones + added here use `str_starts_with` / `str_ends_with`. +- **Do not build fixtures out of leaky helpers.** `substr` and `md5` on a Mixed leak (this + document's open debt), and a large expected-value local never gets reclaimed. Either will + make a heap-clean fixture impossible to satisfy for reasons unrelated to what it tests. +- **Treat any builtin sweep as a lower bound.** An automated sweep performed during this + work missed at least one leaking position (`wordwrap`'s third parameter) and produced + bucket counts that could not be reproduced from its own artifacts. Re-derive from the + registry rather than trusting a list. +- **macOS absorbs double frees.** A clean heap-debug run on macOS aarch64 does not prove the + absence of a double free; only the linux-x86_64 heap-debug path escalates it to a fatal. + +## Verification status + +The two fixes were proven failing-before and passing-after with exact figures +(`4 blocks / 2097216 B` and `1 block / 262076 B`, both to `clean`), each with an +arena-sized negative control that passes on both sides. + +Suites run green on the changed paths: 662 tests across `runtime_gc`, `io::streams`, +`io::filesystem` and `io::files`, plus 1110 library, 1181 binary, 8 parity and 4 cdylib +tests. The full `codegen_tests` binary was not run to completion on the development host — +its `eval` module exhausted local disk twice — but reported zero failures across the 4533 +and 1338 tests it did execute before being killed. + +**Outstanding: the x86_64 arm has never been executed.** Emission was verified (the correct +frame slots appear at all six URL sites), but no linux-x86_64 `--heap-debug` run was +performed, and per the pitfall above that is exactly the platform that would catch a double +free. That run should be treated as required before considering this work complete. diff --git a/examples/process-pipes/.gitignore b/examples/process-pipes/.gitignore new file mode 100644 index 0000000000..f452a788bd --- /dev/null +++ b/examples/process-pipes/.gitignore @@ -0,0 +1,4 @@ +*.s +*.o +main +main.exe diff --git a/examples/process-pipes/main.php b/examples/process-pipes/main.php new file mode 100644 index 0000000000..df9cd2ed12 --- /dev/null +++ b/examples/process-pipes/main.php @@ -0,0 +1,37 @@ + ["pipe", "r"], + 1 => ["pipe", "w"], + 2 => ["pipe", "w"], + ], + $pipes, +); + +if ($process === false) { + echo "Could not start sort\n"; + exit(1); +} + +fwrite($pipes[0], "pear\napple\norange\n"); +fclose($pipes[0]); + +$sorted = fread($pipes[1], 4096); +$errors = fread($pipes[2], 4096); +fclose($pipes[1]); +fclose($pipes[2]); + +$status = proc_get_status($process); +$exitCode = proc_close($process); + +echo $sorted; +echo "running before close: " . ($status["running"] ? "yes" : "no") . "\n"; +echo "exit code: " . $exitCode . "\n"; + +if (strlen($errors) > 0) { + echo "stderr: " . $errors; +} diff --git a/examples/shell-escaping/.gitignore b/examples/shell-escaping/.gitignore new file mode 100644 index 0000000000..f452a788bd --- /dev/null +++ b/examples/shell-escaping/.gitignore @@ -0,0 +1,4 @@ +*.s +*.o +main +main.exe diff --git a/examples/shell-escaping/main.php b/examples/shell-escaping/main.php new file mode 100644 index 0000000000..0a1747b077 --- /dev/null +++ b/examples/shell-escaping/main.php @@ -0,0 +1,7 @@ + set[str]: + """Return the no-mangle elephc C ABI functions declared below source_dir.""" + exports: set[str] = set() + for path in sorted(source_dir.rglob("*.rs")): + exports.update(EXPORT_RE.findall(path.read_text(encoding="utf-8"))) + return exports + + +def archive_exports(nm: str, archive: Path) -> set[str]: + """Return elephc symbols defined globally by archive according to nm.""" + result = subprocess.run( + [nm, "-g", "--defined-only", str(archive)], + check=False, + capture_output=True, + text=True, + ) + if result.returncode != 0: + sys.stderr.write(result.stderr) + raise RuntimeError(f"{nm} failed for {archive} with status {result.returncode}") + + exports: set[str] = set() + for line in result.stdout.splitlines(): + if not line.split(): + continue + match = NM_SYMBOL_RE.match(line.split()[-1]) + if match: + exports.add(match.group(1)) + return exports + + +def default_nm(target: str) -> str: + """Choose an nm implementation appropriate for target, if available.""" + candidates = [] + if target: + candidates.append(f"{target}-nm") + if target == "x86_64-pc-windows-gnu": + candidates.append("x86_64-w64-mingw32-nm") + candidates.extend(("llvm-nm", "nm")) + for candidate in candidates: + resolved = shutil.which(candidate) + if resolved: + return resolved + raise RuntimeError("no nm implementation found; pass --nm explicitly") + + +def archive_path(target_dir: Path, target: str, profile: str, bridge: str) -> Path: + """Resolve Cargo's staticlib archive path for one bridge build.""" + base = target_dir / target if target else target_dir + directory = base / profile + candidates = ( + directory / f"libelephc_{bridge}.a", + directory / f"elephc_{bridge}.lib", + directory / f"libelephc_{bridge}.lib", + ) + return next((path for path in candidates if path.is_file()), candidates[0]) + + +def check_bridge(bridge: str, target_dir: Path, target: str, profile: str, nm: str) -> bool: + """Compare one bridge's source ABI with its built archive, printing drift.""" + source_dir = ROOT / "crates" / f"elephc-{bridge}" / "src" + archive = archive_path(target_dir, target, profile, bridge) + if not archive.is_file(): + print(f"ERROR [{bridge}]: archive not found: {archive}", file=sys.stderr) + return False + + intended = source_exports(source_dir) + if not intended: + print(f"ERROR [{bridge}]: no C ABI exports found below {source_dir}", file=sys.stderr) + return False + actual = archive_exports(nm, archive) + missing = sorted(intended - actual) + unexpected = sorted(actual - intended) + + for symbol in missing: + print(f"ERROR [{bridge}]: archive is missing {symbol}", file=sys.stderr) + for symbol in unexpected: + print(f"ERROR [{bridge}]: archive has undeclared export {symbol}", file=sys.stderr) + print( + f"[{bridge}] source={len(intended)} archive={len(actual)} " + f"missing={len(missing)} unexpected={len(unexpected)}" + ) + return not missing and not unexpected + + +def parse_args() -> argparse.Namespace: + """Parse command-line options for archive location and bridge selection.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--target", default="", help="Cargo target triple") + parser.add_argument("--profile", default="debug", help="Cargo profile directory") + parser.add_argument("--target-dir", type=Path, default=ROOT / "target") + parser.add_argument("--nm", help="nm-compatible executable") + parser.add_argument("--bridge", action="append", choices=BRIDGES, dest="bridges") + return parser.parse_args() + + +def main() -> int: + """Check the requested bridge archives and return nonzero on any ABI drift.""" + args = parse_args() + try: + nm = args.nm or default_nm(args.target) + results = [ + check_bridge(bridge, args.target_dir, args.target, args.profile, nm) + for bridge in (args.bridges or BRIDGES) + ] + ok = all(results) + except (OSError, RuntimeError) as error: + print(f"ERROR: {error}", file=sys.stderr) + return 1 + return 0 if ok else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/docs/builtin_registry.json b/scripts/docs/builtin_registry.json index 58a03e2a27..61ff4c7a82 100644 --- a/scripts/docs/builtin_registry.json +++ b/scripts/docs/builtin_registry.json @@ -8,6 +8,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": false, "is_extension": false, "is_internal": true, @@ -16,7 +17,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Internal helper used by the gmmktime() builtin.", "Bypasses timezone handling and calls the runtime gmmktime helper directly.", @@ -30,6 +31,8 @@ "sig_line": null }, "name": "__elephc_gmmktime_raw", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -143,6 +146,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": false, "is_extension": false, "is_internal": true, @@ -151,7 +155,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Internal helper used by the mktime() builtin.", "Bypasses timezone handling and calls the runtime mktime helper directly.", @@ -165,6 +169,8 @@ "sig_line": null }, "name": "__elephc_mktime_raw", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -278,6 +284,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": false, "is_extension": false, "is_internal": true, @@ -286,7 +293,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_bzip2_archive` through `BuiltinLoweringContext`.", @@ -298,6 +305,8 @@ "sig_line": null }, "name": "__elephc_phar_bzip2_archive", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -381,6 +390,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": false, "is_extension": false, "is_internal": true, @@ -389,7 +399,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_decompress_archive` through `BuiltinLoweringContext`.", @@ -401,6 +411,8 @@ "sig_line": null }, "name": "__elephc_phar_decompress_archive", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -484,6 +496,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": false, "is_extension": false, "is_internal": true, @@ -492,7 +505,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_get_file_metadata` through `BuiltinLoweringContext`.", @@ -504,6 +517,8 @@ "sig_line": null }, "name": "__elephc_phar_get_file_metadata", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -587,6 +602,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": false, "is_extension": false, "is_internal": true, @@ -595,7 +611,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_get_metadata` through `BuiltinLoweringContext`.", @@ -607,6 +623,8 @@ "sig_line": null }, "name": "__elephc_phar_get_metadata", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -690,6 +708,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": false, "is_extension": false, "is_internal": true, @@ -698,7 +717,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_get_signature_hash` through `BuiltinLoweringContext`.", @@ -710,6 +729,8 @@ "sig_line": null }, "name": "__elephc_phar_get_signature_hash", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -793,6 +814,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": false, "is_extension": false, "is_internal": true, @@ -801,7 +823,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_get_signature_type` through `BuiltinLoweringContext`.", @@ -813,6 +835,8 @@ "sig_line": null }, "name": "__elephc_phar_get_signature_type", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -896,6 +920,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": false, "is_extension": false, "is_internal": true, @@ -904,7 +929,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_get_stub` through `BuiltinLoweringContext`.", @@ -916,6 +941,8 @@ "sig_line": null }, "name": "__elephc_phar_get_stub", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -999,6 +1026,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": false, "is_extension": false, "is_internal": true, @@ -1007,7 +1035,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_gzip_archive` through `BuiltinLoweringContext`.", @@ -1019,6 +1047,8 @@ "sig_line": null }, "name": "__elephc_phar_gzip_archive", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -1102,6 +1132,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": false, "is_extension": false, "is_internal": true, @@ -1110,7 +1141,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "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.", @@ -1124,6 +1155,8 @@ "sig_line": null }, "name": "__elephc_phar_list_entries", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -1208,6 +1241,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": false, "is_extension": false, "is_internal": true, @@ -1216,7 +1250,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "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.", @@ -1230,6 +1264,8 @@ "sig_line": null }, "name": "__elephc_phar_set_compression", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -1320,6 +1356,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": false, "is_extension": false, "is_internal": true, @@ -1328,7 +1365,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_set_file_metadata` through `BuiltinLoweringContext`.", @@ -1340,6 +1377,8 @@ "sig_line": null }, "name": "__elephc_phar_set_file_metadata", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -1430,6 +1469,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": false, "is_extension": false, "is_internal": true, @@ -1438,7 +1478,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_set_metadata` through `BuiltinLoweringContext`.", @@ -1450,6 +1490,8 @@ "sig_line": null }, "name": "__elephc_phar_set_metadata", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -1540,6 +1582,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": false, "is_extension": false, "is_internal": true, @@ -1548,7 +1591,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_set_stub` through `BuiltinLoweringContext`.", @@ -1560,6 +1603,8 @@ "sig_line": null }, "name": "__elephc_phar_set_stub", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -1650,6 +1695,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": false, "is_extension": false, "is_internal": true, @@ -1658,7 +1704,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_set_zip_password` through `BuiltinLoweringContext`.", @@ -1670,6 +1716,8 @@ "sig_line": null }, "name": "__elephc_phar_set_zip_password", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -1753,6 +1801,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": false, "is_extension": false, "is_internal": true, @@ -1761,7 +1810,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_sign_hash` through `BuiltinLoweringContext`.", @@ -1773,6 +1822,8 @@ "sig_line": null }, "name": "__elephc_phar_sign_hash", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -1845,7 +1896,7 @@ "default": null, "name": "algo", "optional": false, - "type": "string" + "type": "int" } ], "return_type": "bool", @@ -1863,6 +1914,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": false, "is_extension": false, "is_internal": true, @@ -1871,7 +1923,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_sign_openssl` through `BuiltinLoweringContext`.", @@ -1883,6 +1935,8 @@ "sig_line": null }, "name": "__elephc_phar_sign_openssl", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -1973,6 +2027,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": false, "is_extension": false, "is_internal": true, @@ -1981,7 +2036,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_ptr_is_null` through `BuiltinLoweringContext`.", @@ -1993,6 +2048,8 @@ "sig_line": null }, "name": "__elephc_ptr_is_null", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -2072,6 +2129,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": false, "is_extension": false, "is_internal": true, @@ -2080,7 +2138,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_ptr_read_string` through `BuiltinLoweringContext`.", @@ -2092,6 +2150,8 @@ "sig_line": null }, "name": "__elephc_ptr_read_string", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -2178,6 +2238,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": false, "is_extension": false, "is_internal": true, @@ -2186,7 +2247,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_ptr_write_string` through `BuiltinLoweringContext`.", @@ -2198,6 +2259,8 @@ "sig_line": null }, "name": "__elephc_ptr_write_string", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -2284,6 +2347,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": false, "is_extension": false, "is_internal": true, @@ -2292,7 +2356,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Internal helper used by the strtotime() builtin.", "Provides a raw timestamp parsing path for the runtime strtotime helper.", @@ -2306,6 +2370,8 @@ "sig_line": null }, "name": "__elephc_strtotime_raw", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -2342,8 +2408,7 @@ "kind": "may_alias_arguments" }, "requirements": { - "kind": "static", - "values": [] + "kind": "shared" }, "result_type": "declared", "runtime_functions": [ @@ -2407,6 +2472,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -2415,7 +2481,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.abs` through `BuiltinLoweringContext`.", @@ -2427,6 +2493,8 @@ "sig_line": null }, "name": "abs", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -2505,6 +2573,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -2513,7 +2582,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.acos` through `BuiltinLoweringContext`.", @@ -2525,6 +2594,8 @@ "sig_line": null }, "name": "acos", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -2602,6 +2673,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -2610,7 +2682,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.string.add_slashes` through `BuiltinLoweringContext`.", @@ -2622,6 +2694,8 @@ "sig_line": null }, "name": "addslashes", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -2680,6 +2754,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -2688,7 +2763,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_all` through `BuiltinLoweringContext`.", @@ -2700,6 +2775,8 @@ "sig_line": null }, "name": "array_all", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -2786,6 +2863,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -2794,7 +2872,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_any` through `BuiltinLoweringContext`.", @@ -2806,6 +2884,8 @@ "sig_line": null }, "name": "array_any", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -2914,6 +2994,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -2922,7 +3003,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_chunk` through `BuiltinLoweringContext`.", @@ -2934,6 +3015,8 @@ "sig_line": null }, "name": "array_chunk", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -3025,6 +3108,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -3033,7 +3117,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_column` through `BuiltinLoweringContext`.", @@ -3045,6 +3129,8 @@ "sig_line": null }, "name": "array_column", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -3136,6 +3222,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -3144,7 +3231,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_combine` through `BuiltinLoweringContext`.", @@ -3156,6 +3243,8 @@ "sig_line": null }, "name": "array_combine", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -3241,6 +3330,7 @@ "variadic": "arrays" }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -3249,7 +3339,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_diff` through `BuiltinLoweringContext`.", @@ -3261,6 +3351,8 @@ "sig_line": null }, "name": "array_diff", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -3323,6 +3415,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -3331,7 +3424,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_diff_assoc` through `BuiltinLoweringContext`.", @@ -3343,6 +3436,8 @@ "sig_line": null }, "name": "array_diff_assoc", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -3421,6 +3516,7 @@ "variadic": "arrays" }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -3429,7 +3525,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_diff_key` through `BuiltinLoweringContext`.", @@ -3441,6 +3537,8 @@ "sig_line": null }, "name": "array_diff_key", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -3531,6 +3629,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -3539,7 +3638,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_fill` through `BuiltinLoweringContext`.", @@ -3551,6 +3650,8 @@ "sig_line": null }, "name": "array_fill", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -3649,6 +3750,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -3657,7 +3759,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_fill_keys` through `BuiltinLoweringContext`.", @@ -3669,6 +3771,8 @@ "sig_line": null }, "name": "array_fill_keys", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -3766,6 +3870,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -3774,7 +3879,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_filter` through `BuiltinLoweringContext`.", @@ -3786,6 +3891,8 @@ "sig_line": null }, "name": "array_filter", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -3879,6 +3986,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -3887,7 +3995,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_find` through `BuiltinLoweringContext`.", @@ -3899,6 +4007,8 @@ "sig_line": null }, "name": "array_find", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -4001,6 +4111,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -4009,7 +4120,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_flip` through `BuiltinLoweringContext`.", @@ -4021,6 +4132,8 @@ "sig_line": null }, "name": "array_flip", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -4099,6 +4212,7 @@ "variadic": "arrays" }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -4107,7 +4221,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_intersect` through `BuiltinLoweringContext`.", @@ -4119,6 +4233,8 @@ "sig_line": null }, "name": "array_intersect", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -4181,6 +4297,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -4189,7 +4306,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_intersect_assoc` through `BuiltinLoweringContext`.", @@ -4201,6 +4318,8 @@ "sig_line": null }, "name": "array_intersect_assoc", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -4279,6 +4398,7 @@ "variadic": "arrays" }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -4287,7 +4407,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_intersect_key` through `BuiltinLoweringContext`.", @@ -4299,6 +4419,8 @@ "sig_line": null }, "name": "array_intersect_key", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -4361,6 +4483,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -4369,7 +4492,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_is_list` through `BuiltinLoweringContext`.", @@ -4381,6 +4504,8 @@ "sig_line": null }, "name": "array_is_list", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -4465,6 +4590,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -4473,7 +4599,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_key_exists` through `BuiltinLoweringContext`.", @@ -4485,6 +4611,8 @@ "sig_line": null }, "name": "array_key_exists", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -4554,6 +4682,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -4562,7 +4691,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_key_first` through `BuiltinLoweringContext`.", @@ -4574,6 +4703,8 @@ "sig_line": null }, "name": "array_key_first", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -4636,6 +4767,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -4644,7 +4776,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_key_last` through `BuiltinLoweringContext`.", @@ -4656,6 +4788,8 @@ "sig_line": null }, "name": "array_key_last", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -4734,6 +4868,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -4742,7 +4877,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_keys` through `BuiltinLoweringContext`.", @@ -4754,6 +4889,8 @@ "sig_line": null }, "name": "array_keys", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -4838,6 +4975,7 @@ "variadic": "arrays" }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -4846,7 +4984,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_map` through `BuiltinLoweringContext`.", @@ -4858,6 +4996,8 @@ "sig_line": null }, "name": "array_map", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -4953,6 +5093,7 @@ "variadic": "arrays" }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -4961,7 +5102,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_merge` through `BuiltinLoweringContext`.", @@ -4973,6 +5114,8 @@ "sig_line": null }, "name": "array_merge", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -5027,6 +5170,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -5035,7 +5179,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_merge_recursive` through `BuiltinLoweringContext`.", @@ -5047,6 +5191,8 @@ "sig_line": null }, "name": "array_merge_recursive", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -5101,6 +5247,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -5109,7 +5256,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_multisort` through `BuiltinLoweringContext`.", @@ -5121,6 +5268,8 @@ "sig_line": null }, "name": "array_multisort", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -5235,6 +5384,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -5243,7 +5393,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_pad` through `BuiltinLoweringContext`.", @@ -5255,6 +5405,8 @@ "sig_line": null }, "name": "array_pad", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -5346,6 +5498,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -5354,7 +5507,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_pop` through `BuiltinLoweringContext`.", @@ -5366,6 +5519,8 @@ "sig_line": null }, "name": "array_pop", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -5461,6 +5616,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -5469,7 +5625,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_product` through `BuiltinLoweringContext`.", @@ -5481,6 +5637,8 @@ "sig_line": null }, "name": "array_product", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -5558,6 +5716,7 @@ "variadic": "values" }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -5566,7 +5725,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_push` through `BuiltinLoweringContext`.", @@ -5578,6 +5737,8 @@ "sig_line": null }, "name": "array_push", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -5673,6 +5834,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -5681,7 +5843,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_rand` through `BuiltinLoweringContext`.", @@ -5693,6 +5855,8 @@ "sig_line": null }, "name": "array_rand", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -5800,6 +5964,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -5808,7 +5973,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_reduce` through `BuiltinLoweringContext`.", @@ -5820,6 +5985,8 @@ "sig_line": null }, "name": "array_reduce", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -5913,6 +6080,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -5921,7 +6089,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_replace` through `BuiltinLoweringContext`.", @@ -5933,6 +6101,8 @@ "sig_line": null }, "name": "array_replace", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -6002,6 +6172,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -6010,7 +6181,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_replace_recursive` through `BuiltinLoweringContext`.", @@ -6022,6 +6193,8 @@ "sig_line": null }, "name": "array_replace_recursive", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -6113,6 +6286,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -6121,7 +6295,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_reverse` through `BuiltinLoweringContext`.", @@ -6133,6 +6307,8 @@ "sig_line": null }, "name": "array_reverse", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -6223,6 +6399,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -6231,7 +6408,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_search` through `BuiltinLoweringContext`.", @@ -6243,6 +6420,8 @@ "sig_line": null }, "name": "array_search", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -6334,6 +6513,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -6342,7 +6522,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_shift` through `BuiltinLoweringContext`.", @@ -6354,6 +6534,8 @@ "sig_line": null }, "name": "array_shift", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -6461,6 +6643,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -6469,7 +6652,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_slice` through `BuiltinLoweringContext`.", @@ -6481,6 +6664,8 @@ "sig_line": null }, "name": "array_slice", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -6590,6 +6775,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -6598,7 +6784,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_splice` through `BuiltinLoweringContext`.", @@ -6610,6 +6796,8 @@ "sig_line": null }, "name": "array_splice", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -6719,6 +6907,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -6727,7 +6916,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_sum` through `BuiltinLoweringContext`.", @@ -6739,6 +6928,8 @@ "sig_line": null }, "name": "array_sum", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -6801,6 +6992,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -6809,7 +7001,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_udiff` through `BuiltinLoweringContext`.", @@ -6821,6 +7013,8 @@ "sig_line": null }, "name": "array_udiff", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -6914,6 +7108,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -6922,7 +7117,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_uintersect` through `BuiltinLoweringContext`.", @@ -6934,6 +7129,8 @@ "sig_line": null }, "name": "array_uintersect", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -7043,6 +7240,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -7051,7 +7249,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_unique` through `BuiltinLoweringContext`.", @@ -7063,6 +7261,8 @@ "sig_line": null }, "name": "array_unique", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -7140,6 +7340,7 @@ "variadic": "values" }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -7148,7 +7349,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_unshift` through `BuiltinLoweringContext`.", @@ -7160,6 +7361,8 @@ "sig_line": null }, "name": "array_unshift", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -7255,6 +7458,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -7263,7 +7467,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_values` through `BuiltinLoweringContext`.", @@ -7275,6 +7479,8 @@ "sig_line": null }, "name": "array_values", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -7358,6 +7564,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -7366,7 +7573,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_walk` through `BuiltinLoweringContext`.", @@ -7378,6 +7585,8 @@ "sig_line": null }, "name": "array_walk", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -7464,6 +7673,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -7472,7 +7682,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_walk_recursive` through `BuiltinLoweringContext`.", @@ -7484,6 +7694,8 @@ "sig_line": null }, "name": "array_walk_recursive", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -7585,6 +7797,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -7593,7 +7806,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.arsort` through `BuiltinLoweringContext`.", @@ -7605,6 +7818,8 @@ "sig_line": null }, "name": "arsort", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -7700,6 +7915,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -7708,7 +7924,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.asin` through `BuiltinLoweringContext`.", @@ -7720,6 +7936,8 @@ "sig_line": null }, "name": "asin", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -7796,6 +8014,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -7804,7 +8023,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.asort` through `BuiltinLoweringContext`.", @@ -7816,6 +8035,8 @@ "sig_line": null }, "name": "asort", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -7911,6 +8132,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -7919,7 +8141,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.atan` through `BuiltinLoweringContext`.", @@ -7931,6 +8153,8 @@ "sig_line": null }, "name": "atan", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -8014,6 +8238,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -8022,7 +8247,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.atan2` through `BuiltinLoweringContext`.", @@ -8034,6 +8259,8 @@ "sig_line": null }, "name": "atan2", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -8118,6 +8345,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -8126,7 +8354,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.string.base64_decode` through `BuiltinLoweringContext`.", @@ -8138,6 +8366,8 @@ "sig_line": null }, "name": "base64_decode", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -8212,6 +8442,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -8220,7 +8451,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.string.base64_encode` through `BuiltinLoweringContext`.", @@ -8232,6 +8463,8 @@ "sig_line": null }, "name": "base64_encode", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -8312,6 +8545,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -8320,7 +8554,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.basename` through `BuiltinLoweringContext`.", @@ -8332,6 +8566,8 @@ "sig_line": null }, "name": "basename", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -8433,6 +8669,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -8441,7 +8678,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.string.bin_to_hex` through `BuiltinLoweringContext`.", @@ -8453,6 +8690,8 @@ "sig_line": null }, "name": "bin2hex", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -8527,6 +8766,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -8535,7 +8775,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -8546,6 +8786,8 @@ "sig_line": null }, "name": "boolval", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -8618,6 +8860,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": true, "is_internal": false, @@ -8626,7 +8869,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.buffer_free` through `BuiltinLoweringContext`.", @@ -8638,6 +8881,8 @@ "sig_line": null }, "name": "buffer_free", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -8733,6 +8978,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": true, "is_internal": false, @@ -8741,7 +8987,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.buffer_len` through `BuiltinLoweringContext`.", @@ -8753,6 +8999,8 @@ "sig_line": null }, "name": "buffer_len", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -8848,6 +9096,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": true, "is_internal": false, @@ -8864,6 +9113,8 @@ "sig_line": null }, "name": "buffer_new", + "notes": [], + "see_also": [], "semantics": null, "sig": { "params": [ @@ -8906,6 +9157,7 @@ "variadic": "args" }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -8914,7 +9166,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.call_user_func` through `BuiltinLoweringContext`.", @@ -8926,6 +9178,8 @@ "sig_line": null }, "name": "call_user_func", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -9027,6 +9281,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -9035,7 +9290,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.call_user_func_array` through `BuiltinLoweringContext`.", @@ -9047,6 +9302,8 @@ "sig_line": null }, "name": "call_user_func_array", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -9149,6 +9406,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -9157,7 +9415,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ceil` through `BuiltinLoweringContext`.", @@ -9169,6 +9427,8 @@ "sig_line": null }, "name": "ceil", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -9246,6 +9506,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -9254,7 +9515,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.chdir` through `BuiltinLoweringContext`.", @@ -9266,6 +9527,8 @@ "sig_line": null }, "name": "chdir", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -9372,6 +9635,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -9380,7 +9644,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.checkdate` through `BuiltinLoweringContext`.", @@ -9392,6 +9656,8 @@ "sig_line": null }, "name": "checkdate", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -9506,6 +9772,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -9514,7 +9781,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.chgrp` through `BuiltinLoweringContext`.", @@ -9526,6 +9793,8 @@ "sig_line": null }, "name": "chgrp", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -9594,7 +9863,7 @@ "default": null, "name": "group", "optional": false, - "type": "string" + "type": "mixed" } ], "return_type": "bool", @@ -9634,6 +9903,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -9642,7 +9912,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.chmod` through `BuiltinLoweringContext`.", @@ -9654,6 +9924,8 @@ "sig_line": null }, "name": "chmod", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -9762,6 +10034,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -9770,7 +10043,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.chop` through `BuiltinLoweringContext`.", @@ -9782,6 +10055,8 @@ "sig_line": null }, "name": "chop", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -9872,6 +10147,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -9880,7 +10156,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.chown` through `BuiltinLoweringContext`.", @@ -9892,6 +10168,8 @@ "sig_line": null }, "name": "chown", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -9960,7 +10238,7 @@ "default": null, "name": "user", "optional": false, - "type": "string" + "type": "mixed" } ], "return_type": "bool", @@ -9994,6 +10272,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -10002,7 +10281,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.chr` through `BuiltinLoweringContext`.", @@ -10014,6 +10293,8 @@ "sig_line": null }, "name": "chr", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -10103,6 +10384,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -10111,7 +10393,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.clamp` through `BuiltinLoweringContext`.", @@ -10123,6 +10405,8 @@ "sig_line": null }, "name": "clamp", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -10243,6 +10527,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -10251,7 +10536,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.class_alias` through `BuiltinLoweringContext`.", @@ -10263,6 +10548,8 @@ "sig_line": null }, "name": "class_alias", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -10378,6 +10665,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": true, "is_internal": false, @@ -10386,7 +10674,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.class_attribute_args` through `BuiltinLoweringContext`.", @@ -10398,6 +10686,8 @@ "sig_line": null }, "name": "class_attribute_args", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -10500,6 +10790,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": true, "is_internal": false, @@ -10508,7 +10799,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.class_attribute_names` through `BuiltinLoweringContext`.", @@ -10520,6 +10811,8 @@ "sig_line": null }, "name": "class_attribute_names", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -10621,6 +10914,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -10629,7 +10923,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.class_exists` through `BuiltinLoweringContext`.", @@ -10641,6 +10935,8 @@ "sig_line": null }, "name": "class_exists", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -10743,6 +11039,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": true, "is_internal": false, @@ -10751,7 +11048,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.class_get_attributes` through `BuiltinLoweringContext`.", @@ -10763,6 +11060,8 @@ "sig_line": null }, "name": "class_get_attributes", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -10864,6 +11163,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -10872,7 +11172,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.class_implements` through `BuiltinLoweringContext`.", @@ -10884,6 +11184,8 @@ "sig_line": null }, "name": "class_implements", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -10992,6 +11294,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -11000,7 +11303,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.class_parents` through `BuiltinLoweringContext`.", @@ -11012,6 +11315,8 @@ "sig_line": null }, "name": "class_parents", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -11120,6 +11425,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -11128,7 +11434,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.class_uses` through `BuiltinLoweringContext`.", @@ -11140,6 +11446,8 @@ "sig_line": null }, "name": "class_uses", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -11248,6 +11556,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -11256,7 +11565,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.clearstatcache` through `BuiltinLoweringContext`.", @@ -11268,6 +11577,8 @@ "sig_line": null }, "name": "clearstatcache", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -11369,6 +11680,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -11377,7 +11689,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.closedir` through `BuiltinLoweringContext`.", @@ -11389,6 +11701,8 @@ "sig_line": null }, "name": "closedir", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -11490,6 +11804,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -11498,7 +11813,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.copy` through `BuiltinLoweringContext`.", @@ -11510,6 +11825,8 @@ "sig_line": null }, "name": "copy", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -11611,6 +11928,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -11619,7 +11937,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.cos` through `BuiltinLoweringContext`.", @@ -11631,6 +11949,8 @@ "sig_line": null }, "name": "cos", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -11708,6 +12028,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -11716,7 +12037,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.cosh` through `BuiltinLoweringContext`.", @@ -11728,6 +12049,8 @@ "sig_line": null }, "name": "cosh", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -11811,6 +12134,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -11819,7 +12143,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.count` through `BuiltinLoweringContext`.", @@ -11831,6 +12155,8 @@ "sig_line": null }, "name": "count", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "count", "callable": { @@ -11933,6 +12259,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -11941,7 +12268,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.crc32` through `BuiltinLoweringContext`.", @@ -11953,6 +12280,8 @@ "sig_line": null }, "name": "crc32", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -12030,6 +12359,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -12038,7 +12368,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ctype_alnum` through `BuiltinLoweringContext`.", @@ -12050,6 +12380,8 @@ "sig_line": null }, "name": "ctype_alnum", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -12127,6 +12459,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -12135,7 +12468,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ctype_alpha` through `BuiltinLoweringContext`.", @@ -12147,6 +12480,8 @@ "sig_line": null }, "name": "ctype_alpha", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -12224,6 +12559,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -12232,7 +12568,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ctype_digit` through `BuiltinLoweringContext`.", @@ -12244,6 +12580,8 @@ "sig_line": null }, "name": "ctype_digit", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -12321,6 +12659,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -12329,7 +12668,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ctype_space` through `BuiltinLoweringContext`.", @@ -12341,6 +12680,8 @@ "sig_line": null }, "name": "ctype_space", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -12424,6 +12765,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -12432,7 +12774,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.date` through `BuiltinLoweringContext`.", @@ -12444,6 +12786,8 @@ "sig_line": null }, "name": "date", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "date", "callable": { @@ -12480,8 +12824,7 @@ "kind": "may_alias_arguments" }, "requirements": { - "kind": "static", - "values": [] + "kind": "shared" }, "result_type": "declared", "runtime_functions": [ @@ -12538,6 +12881,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -12546,7 +12890,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.date_default_timezone_get` through `BuiltinLoweringContext`.", @@ -12558,6 +12902,8 @@ "sig_line": null }, "name": "date_default_timezone_get", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -12644,6 +12990,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -12652,7 +12999,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.date_default_timezone_set` through `BuiltinLoweringContext`.", @@ -12664,6 +13011,8 @@ "sig_line": null }, "name": "date_default_timezone_set", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -12764,6 +13113,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -12772,7 +13122,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.define` through `BuiltinLoweringContext`.", @@ -12784,6 +13134,8 @@ "sig_line": null }, "name": "define", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -12886,6 +13238,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -12894,7 +13247,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.defined` through `BuiltinLoweringContext`.", @@ -12906,6 +13259,8 @@ "sig_line": null }, "name": "defined", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -13001,6 +13356,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -13009,7 +13365,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.deg2rad` through `BuiltinLoweringContext`.", @@ -13021,6 +13377,8 @@ "sig_line": null }, "name": "deg2rad", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -13098,6 +13456,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -13114,6 +13473,8 @@ "sig_line": null }, "name": "die", + "notes": [], + "see_also": [], "semantics": null, "sig": { "params": [ @@ -13162,6 +13523,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -13170,7 +13532,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.dirname` through `BuiltinLoweringContext`.", @@ -13182,6 +13544,8 @@ "sig_line": null }, "name": "dirname", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -13284,6 +13648,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -13292,7 +13657,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.disk_free_space` through `BuiltinLoweringContext`.", @@ -13304,6 +13669,8 @@ "sig_line": null }, "name": "disk_free_space", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -13398,6 +13765,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -13406,7 +13774,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.disk_total_space` through `BuiltinLoweringContext`.", @@ -13418,6 +13786,8 @@ "sig_line": null }, "name": "disk_total_space", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -13512,6 +13882,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -13520,7 +13891,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_empty", - "codegen_line": 1077, + "codegen_line": 1079, "notes": [ "Lowers `empty()` for concrete scalar and array-like operands." ], @@ -13532,6 +13903,8 @@ "sig_line": null }, "name": "empty", + "notes": [], + "see_also": [], "semantics": null, "sig": { "params": [ @@ -13580,6 +13953,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -13588,7 +13962,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.enum_exists` through `BuiltinLoweringContext`.", @@ -13600,6 +13974,8 @@ "sig_line": null }, "name": "enum_exists", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -13677,6 +14053,218 @@ "slug": "enum_exists", "sub_area": "Class" }, + { + "area": "String", + "canonical_name": "escapeshellarg", + "description": "Quotes one argument for safe use in a shell command.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/escapeshellarg.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "arg", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, + "examples": [ + "See [`examples/shell-escaping`](../../../../examples/shell-escaping/main.php) for argument and command escaping together." + ], + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 426, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.string.escape_shell_arg` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/string/escapeshellarg.rs", + "sig_line": null + }, + "name": "escapeshellarg", + "notes": [ + "macOS and Linux use PHP's single-quoted shell form; Windows uses PHP's double-quoted command-line escaping rules.", + "Embedded NUL bytes raise a catchable `ValueError`.", + "Windows enforces PHP's 8192-byte command-line limit for both input and output." + ], + "see_also": [], + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "dynamic" + }, + "effects": { + "kind": "static", + "names": [ + "may_throw" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "string.escape_shell_arg" + }, + "ownership": { + "argument_indexes": [], + "kind": "fresh" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "checked", + "runtime_functions": [], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "checker_hook", + "lazy": true + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "arg", + "optional": false, + "type": "string" + } + ], + "return_type": "string", + "variadic": null + }, + "slug": "escapeshellarg", + "sub_area": "String" + }, + { + "area": "String", + "canonical_name": "escapeshellcmd", + "description": "Escapes shell metacharacters in a command string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/escapeshellcmd.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "command", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, + "examples": [ + "See [`examples/shell-escaping`](../../../../examples/shell-escaping/main.php) for argument and command escaping together." + ], + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 426, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.string.escape_shell_cmd` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/string/escapeshellcmd.rs", + "sig_line": null + }, + "name": "escapeshellcmd", + "notes": [ + "Shell metacharacters follow PHP's platform-specific POSIX or Windows rules.", + "Embedded NUL bytes raise a catchable `ValueError`.", + "Windows enforces PHP's 8192-byte command-line limit for both input and output." + ], + "see_also": [], + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "dynamic" + }, + "effects": { + "kind": "static", + "names": [ + "may_throw" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "string.escape_shell_cmd" + }, + "ownership": { + "argument_indexes": [], + "kind": "fresh" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "checked", + "runtime_functions": [], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "checker_hook", + "lazy": true + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "command", + "optional": false, + "type": "string" + } + ], + "return_type": "string", + "variadic": null + }, + "slug": "escapeshellcmd", + "sub_area": "String" + }, { "area": "Process", "canonical_name": "exec", @@ -13702,6 +14290,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -13710,7 +14299,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.exec` through `BuiltinLoweringContext`.", @@ -13722,6 +14311,8 @@ "sig_line": null }, "name": "exec", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -13816,6 +14407,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -13832,6 +14424,8 @@ "sig_line": null }, "name": "exit", + "notes": [], + "see_also": [], "semantics": null, "sig": { "params": [ @@ -13874,6 +14468,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -13882,7 +14477,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.exp` through `BuiltinLoweringContext`.", @@ -13894,6 +14489,8 @@ "sig_line": null }, "name": "exp", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -13983,6 +14580,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -13991,7 +14589,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.explode` through `BuiltinLoweringContext`.", @@ -14003,6 +14601,8 @@ "sig_line": null }, "name": "explode", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -14095,6 +14695,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -14103,7 +14704,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fclose` through `BuiltinLoweringContext`.", @@ -14115,6 +14716,8 @@ "sig_line": null }, "name": "fclose", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -14210,6 +14813,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -14218,7 +14822,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fdatasync` through `BuiltinLoweringContext`.", @@ -14230,6 +14834,8 @@ "sig_line": null }, "name": "fdatasync", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -14331,6 +14937,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -14339,7 +14946,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fdiv` through `BuiltinLoweringContext`.", @@ -14351,6 +14958,8 @@ "sig_line": null }, "name": "fdiv", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -14435,6 +15044,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -14443,7 +15053,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.feof` through `BuiltinLoweringContext`.", @@ -14455,6 +15065,8 @@ "sig_line": null }, "name": "feof", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -14550,6 +15162,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -14558,7 +15171,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fflush` through `BuiltinLoweringContext`.", @@ -14570,6 +15183,8 @@ "sig_line": null }, "name": "fflush", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -14665,6 +15280,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -14673,7 +15289,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fgetc` through `BuiltinLoweringContext`.", @@ -14685,6 +15301,8 @@ "sig_line": null }, "name": "fgetc", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -14792,6 +15410,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -14800,7 +15419,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fgetcsv` through `BuiltinLoweringContext`.", @@ -14812,6 +15431,8 @@ "sig_line": null }, "name": "fgetcsv", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -14921,6 +15542,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -14929,7 +15551,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fgets` through `BuiltinLoweringContext`.", @@ -14941,6 +15563,8 @@ "sig_line": null }, "name": "fgets", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -15036,6 +15660,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -15044,7 +15669,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.file` through `BuiltinLoweringContext`.", @@ -15056,6 +15681,8 @@ "sig_line": null }, "name": "file", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -15151,6 +15778,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -15159,7 +15787,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.file_exists` through `BuiltinLoweringContext`.", @@ -15171,6 +15799,8 @@ "sig_line": null }, "name": "file_exists", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -15265,6 +15895,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -15273,7 +15904,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.file_get_contents` through `BuiltinLoweringContext`.", @@ -15285,6 +15916,8 @@ "sig_line": null }, "name": "file_get_contents", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -15385,6 +16018,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -15393,7 +16027,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.file_put_contents` through `BuiltinLoweringContext`.", @@ -15405,6 +16039,8 @@ "sig_line": null }, "name": "file_put_contents", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -15506,6 +16142,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -15514,7 +16151,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fileatime` through `BuiltinLoweringContext`.", @@ -15526,6 +16163,8 @@ "sig_line": null }, "name": "fileatime", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -15621,6 +16260,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -15629,7 +16269,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.filectime` through `BuiltinLoweringContext`.", @@ -15641,6 +16281,8 @@ "sig_line": null }, "name": "filectime", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -15736,6 +16378,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -15744,7 +16387,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.filegroup` through `BuiltinLoweringContext`.", @@ -15756,6 +16399,8 @@ "sig_line": null }, "name": "filegroup", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -15851,6 +16496,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -15859,7 +16505,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fileinode` through `BuiltinLoweringContext`.", @@ -15871,6 +16517,8 @@ "sig_line": null }, "name": "fileinode", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -15966,6 +16614,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -15974,7 +16623,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.filemtime` through `BuiltinLoweringContext`.", @@ -15986,6 +16635,8 @@ "sig_line": null }, "name": "filemtime", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -16049,7 +16700,7 @@ "type": "string" } ], - "return_type": "int", + "return_type": "mixed", "variadic": null }, "slug": "filemtime", @@ -16080,6 +16731,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -16088,7 +16740,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fileowner` through `BuiltinLoweringContext`.", @@ -16100,6 +16752,8 @@ "sig_line": null }, "name": "fileowner", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -16195,6 +16849,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -16203,7 +16858,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fileperms` through `BuiltinLoweringContext`.", @@ -16215,6 +16870,8 @@ "sig_line": null }, "name": "fileperms", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -16310,6 +16967,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -16318,7 +16976,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.filesize` through `BuiltinLoweringContext`.", @@ -16330,6 +16988,8 @@ "sig_line": null }, "name": "filesize", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -16393,7 +17053,7 @@ "type": "string" } ], - "return_type": "int", + "return_type": "mixed", "variadic": null }, "slug": "filesize", @@ -16424,6 +17084,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -16432,7 +17093,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.filetype` through `BuiltinLoweringContext`.", @@ -16444,6 +17105,8 @@ "sig_line": null }, "name": "filetype", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -16539,6 +17202,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -16547,7 +17211,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -16558,6 +17222,8 @@ "sig_line": null }, "name": "floatval", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -16641,6 +17307,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -16649,7 +17316,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.flock` through `BuiltinLoweringContext`.", @@ -16661,6 +17328,8 @@ "sig_line": null }, "name": "flock", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -16770,6 +17439,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -16778,7 +17448,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.floor` through `BuiltinLoweringContext`.", @@ -16790,6 +17460,8 @@ "sig_line": null }, "name": "floor", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -16873,6 +17545,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -16881,7 +17554,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fmod` through `BuiltinLoweringContext`.", @@ -16893,6 +17566,8 @@ "sig_line": null }, "name": "fmod", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -16989,6 +17664,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -16997,7 +17673,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fnmatch` through `BuiltinLoweringContext`.", @@ -17009,6 +17685,8 @@ "sig_line": null }, "name": "fnmatch", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -17136,6 +17814,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -17144,7 +17823,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fopen` through `BuiltinLoweringContext`.", @@ -17156,6 +17835,8 @@ "sig_line": null }, "name": "fopen", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -17271,6 +17952,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -17279,7 +17961,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fpassthru` through `BuiltinLoweringContext`.", @@ -17291,6 +17973,8 @@ "sig_line": null }, "name": "fpassthru", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -17392,6 +18076,7 @@ "variadic": "values" }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -17400,7 +18085,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fprintf` through `BuiltinLoweringContext`.", @@ -17412,6 +18097,8 @@ "sig_line": null }, "name": "fprintf", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -17532,6 +18219,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -17540,7 +18228,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fputcsv` through `BuiltinLoweringContext`.", @@ -17552,6 +18240,8 @@ "sig_line": null }, "name": "fputcsv", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -17674,6 +18364,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -17682,7 +18373,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fread` through `BuiltinLoweringContext`.", @@ -17694,6 +18385,8 @@ "sig_line": null }, "name": "fread", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -17802,6 +18495,7 @@ "variadic": "vars" }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -17810,7 +18504,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fscanf` through `BuiltinLoweringContext`.", @@ -17822,6 +18516,8 @@ "sig_line": null }, "name": "fscanf", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -17936,6 +18632,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -17944,7 +18641,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fseek` through `BuiltinLoweringContext`.", @@ -17956,6 +18653,8 @@ "sig_line": null }, "name": "fseek", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -18088,6 +18787,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -18096,7 +18796,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fsockopen` through `BuiltinLoweringContext`.", @@ -18108,6 +18808,8 @@ "sig_line": null }, "name": "fsockopen", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -18144,8 +18846,7 @@ "kind": "may_alias_arguments" }, "requirements": { - "kind": "static", - "values": [] + "kind": "shared" }, "result_type": "checked", "runtime_functions": [ @@ -18231,6 +18932,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -18239,7 +18941,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fstat` through `BuiltinLoweringContext`.", @@ -18251,6 +18953,8 @@ "sig_line": null }, "name": "fstat", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -18346,6 +19050,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -18354,7 +19059,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fsync` through `BuiltinLoweringContext`.", @@ -18366,6 +19071,8 @@ "sig_line": null }, "name": "fsync", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -18461,6 +19168,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -18469,7 +19177,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ftell` through `BuiltinLoweringContext`.", @@ -18481,6 +19189,8 @@ "sig_line": null }, "name": "ftell", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -18582,6 +19292,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -18590,7 +19301,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ftruncate` through `BuiltinLoweringContext`.", @@ -18602,6 +19313,8 @@ "sig_line": null }, "name": "ftruncate", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -18704,6 +19417,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -18712,7 +19426,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.function_exists` through `BuiltinLoweringContext`.", @@ -18724,6 +19438,8 @@ "sig_line": null }, "name": "function_exists", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -18825,6 +19541,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -18833,7 +19550,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fwrite` through `BuiltinLoweringContext`.", @@ -18845,6 +19562,8 @@ "sig_line": null }, "name": "fwrite", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -18940,6 +19659,7 @@ "variadic": null }, "eval_only": true, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -18956,6 +19676,8 @@ "sig_line": null }, "name": "get_called_class", + "notes": [], + "see_also": [], "semantics": null, "sig": { "params": [], @@ -18990,6 +19712,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -18998,7 +19721,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.get_class` through `BuiltinLoweringContext`.", @@ -19010,6 +19733,8 @@ "sig_line": null }, "name": "get_class", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -19104,6 +19829,7 @@ "variadic": null }, "eval_only": true, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -19120,6 +19846,8 @@ "sig_line": null }, "name": "get_class_methods", + "notes": [], + "see_also": [], "semantics": null, "sig": { "params": [ @@ -19162,6 +19890,7 @@ "variadic": null }, "eval_only": true, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -19178,6 +19907,8 @@ "sig_line": null }, "name": "get_class_vars", + "notes": [], + "see_also": [], "semantics": null, "sig": { "params": [ @@ -19213,6 +19944,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -19221,7 +19953,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.get_declared_classes` through `BuiltinLoweringContext`.", @@ -19233,6 +19965,8 @@ "sig_line": null }, "name": "get_declared_classes", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -19313,6 +20047,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -19321,7 +20056,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.get_declared_interfaces` through `BuiltinLoweringContext`.", @@ -19333,6 +20068,8 @@ "sig_line": null }, "name": "get_declared_interfaces", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -19413,6 +20150,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -19421,7 +20159,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.get_declared_traits` through `BuiltinLoweringContext`.", @@ -19433,6 +20171,8 @@ "sig_line": null }, "name": "get_declared_traits", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -19520,6 +20260,7 @@ "variadic": null }, "eval_only": true, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -19536,6 +20277,8 @@ "sig_line": null }, "name": "get_object_vars", + "notes": [], + "see_also": [], "semantics": null, "sig": { "params": [ @@ -19578,6 +20321,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -19586,7 +20330,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.get_parent_class` through `BuiltinLoweringContext`.", @@ -19598,6 +20342,8 @@ "sig_line": null }, "name": "get_parent_class", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -19692,6 +20438,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -19700,7 +20447,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.get_resource_id` through `BuiltinLoweringContext`.", @@ -19712,6 +20459,8 @@ "sig_line": null }, "name": "get_resource_id", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -19789,6 +20538,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -19797,7 +20547,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.get_resource_type` through `BuiltinLoweringContext`.", @@ -19809,6 +20559,8 @@ "sig_line": null }, "name": "get_resource_type", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -19879,6 +20631,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -19887,7 +20640,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.getcwd` through `BuiltinLoweringContext`.", @@ -19899,6 +20652,8 @@ "sig_line": null }, "name": "getcwd", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -19985,6 +20740,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -19993,7 +20749,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.getdate` through `BuiltinLoweringContext`.", @@ -20005,6 +20761,8 @@ "sig_line": null }, "name": "getdate", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -20099,6 +20857,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -20107,7 +20866,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.getenv` through `BuiltinLoweringContext`.", @@ -20119,6 +20878,8 @@ "sig_line": null }, "name": "getenv", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -20214,6 +20975,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -20222,7 +20984,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.gethostbyaddr` through `BuiltinLoweringContext`.", @@ -20234,6 +20996,8 @@ "sig_line": null }, "name": "gethostbyaddr", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -20329,6 +21093,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -20337,7 +21102,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.gethostbyname` through `BuiltinLoweringContext`.", @@ -20349,6 +21114,8 @@ "sig_line": null }, "name": "gethostbyname", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -20436,6 +21203,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -20444,7 +21212,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.gethostname` through `BuiltinLoweringContext`.", @@ -20456,6 +21224,8 @@ "sig_line": null }, "name": "gethostname", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -20542,6 +21312,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -20550,7 +21321,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.getprotobyname` through `BuiltinLoweringContext`.", @@ -20562,6 +21333,8 @@ "sig_line": null }, "name": "getprotobyname", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -20657,6 +21430,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -20665,7 +21439,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.getprotobynumber` through `BuiltinLoweringContext`.", @@ -20677,6 +21451,8 @@ "sig_line": null }, "name": "getprotobynumber", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -20778,6 +21554,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -20786,7 +21563,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.getservbyname` through `BuiltinLoweringContext`.", @@ -20798,6 +21575,8 @@ "sig_line": null }, "name": "getservbyname", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -20906,6 +21685,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -20914,7 +21694,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.getservbyport` through `BuiltinLoweringContext`.", @@ -20926,6 +21706,8 @@ "sig_line": null }, "name": "getservbyport", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -21028,6 +21810,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -21036,7 +21819,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.gettype` through `BuiltinLoweringContext`.", @@ -21048,6 +21831,8 @@ "sig_line": null }, "name": "gettype", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -21125,6 +21910,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -21133,7 +21919,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.glob` through `BuiltinLoweringContext`.", @@ -21145,6 +21931,8 @@ "sig_line": null }, "name": "glob", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -21246,6 +22034,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -21254,7 +22043,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.gmdate` through `BuiltinLoweringContext`.", @@ -21266,6 +22055,8 @@ "sig_line": null }, "name": "gmdate", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -21302,8 +22093,7 @@ "kind": "may_alias_arguments" }, "requirements": { - "kind": "static", - "values": [] + "kind": "shared" }, "result_type": "declared", "runtime_functions": [ @@ -21397,6 +22187,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -21405,7 +22196,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.gmmktime` through `BuiltinLoweringContext`.", @@ -21417,6 +22208,8 @@ "sig_line": null }, "name": "gmmktime", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -21453,8 +22246,7 @@ "kind": "may_alias_arguments" }, "requirements": { - "kind": "static", - "values": [] + "kind": "shared" }, "result_type": "declared", "runtime_functions": [ @@ -21546,6 +22338,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -21554,7 +22347,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.grapheme_strrev` through `BuiltinLoweringContext`.", @@ -21566,6 +22359,8 @@ "sig_line": null }, "name": "grapheme_strrev", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -21650,6 +22445,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -21658,7 +22454,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.gzcompress` through `BuiltinLoweringContext`.", @@ -21670,6 +22466,8 @@ "sig_line": null }, "name": "gzcompress", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -21782,6 +22580,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -21790,7 +22589,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.gzdeflate` through `BuiltinLoweringContext`.", @@ -21802,6 +22601,8 @@ "sig_line": null }, "name": "gzdeflate", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -21914,6 +22715,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -21922,7 +22724,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.gzinflate` through `BuiltinLoweringContext`.", @@ -21934,6 +22736,8 @@ "sig_line": null }, "name": "gzinflate", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -22047,6 +22851,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -22055,7 +22860,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.gzuncompress` through `BuiltinLoweringContext`.", @@ -22067,6 +22872,8 @@ "sig_line": null }, "name": "gzuncompress", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -22186,6 +22993,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -22194,7 +23002,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.hash` through `BuiltinLoweringContext`.", @@ -22206,6 +23014,8 @@ "sig_line": null }, "name": "hash", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -22312,6 +23122,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -22320,7 +23131,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.hash_algos` through `BuiltinLoweringContext`.", @@ -22332,6 +23143,8 @@ "sig_line": null }, "name": "hash_algos", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -22402,6 +23215,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -22410,7 +23224,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.hash_copy` through `BuiltinLoweringContext`.", @@ -22422,6 +23236,8 @@ "sig_line": null }, "name": "hash_copy", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -22527,6 +23343,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -22535,7 +23352,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.hash_equals` through `BuiltinLoweringContext`.", @@ -22547,6 +23364,8 @@ "sig_line": null }, "name": "hash_equals", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -22643,6 +23462,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -22651,7 +23471,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.hash_file` through `BuiltinLoweringContext`.", @@ -22663,6 +23483,8 @@ "sig_line": null }, "name": "hash_file", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -22783,6 +23605,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -22791,7 +23614,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.hash_final` through `BuiltinLoweringContext`.", @@ -22803,6 +23626,8 @@ "sig_line": null }, "name": "hash_final", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -22927,6 +23752,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -22935,7 +23761,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.hash_hmac` through `BuiltinLoweringContext`.", @@ -22947,6 +23773,8 @@ "sig_line": null }, "name": "hash_hmac", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -23079,6 +23907,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -23087,7 +23916,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.hash_init` through `BuiltinLoweringContext`.", @@ -23099,6 +23928,8 @@ "sig_line": null }, "name": "hash_init", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -23218,6 +24049,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -23226,7 +24058,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.hash_update` through `BuiltinLoweringContext`.", @@ -23238,6 +24070,8 @@ "sig_line": null }, "name": "hash_update", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -23356,6 +24190,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -23364,7 +24199,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.header` through `BuiltinLoweringContext`.", @@ -23376,6 +24211,8 @@ "sig_line": null }, "name": "header", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -23484,6 +24321,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -23492,7 +24330,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.string.hex_to_bin` through `BuiltinLoweringContext`.", @@ -23504,6 +24342,8 @@ "sig_line": null }, "name": "hex2bin", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -23580,6 +24420,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -23588,7 +24429,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.hrtime` through `BuiltinLoweringContext`.", @@ -23600,6 +24441,8 @@ "sig_line": null }, "name": "hrtime", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -23694,6 +24537,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -23702,7 +24546,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.string.html_entity_decode` through `BuiltinLoweringContext`.", @@ -23714,6 +24558,8 @@ "sig_line": null }, "name": "html_entity_decode", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -23800,6 +24646,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -23808,7 +24655,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.htmlentities` through `BuiltinLoweringContext`.", @@ -23820,6 +24667,8 @@ "sig_line": null }, "name": "htmlentities", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -23923,6 +24772,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -23931,7 +24781,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.htmlspecialchars` through `BuiltinLoweringContext`.", @@ -23943,6 +24793,8 @@ "sig_line": null }, "name": "htmlspecialchars", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -24034,6 +24886,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -24042,7 +24895,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.http_response_code` through `BuiltinLoweringContext`.", @@ -24054,6 +24907,8 @@ "sig_line": null }, "name": "http_response_code", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -24154,6 +25009,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -24162,7 +25018,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.hypot` through `BuiltinLoweringContext`.", @@ -24174,6 +25030,8 @@ "sig_line": null }, "name": "hypot", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -24264,6 +25122,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -24272,7 +25131,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.implode` through `BuiltinLoweringContext`.", @@ -24284,6 +25143,8 @@ "sig_line": null }, "name": "implode", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -24381,6 +25242,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -24389,7 +25251,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.in_array` through `BuiltinLoweringContext`.", @@ -24401,6 +25263,8 @@ "sig_line": null }, "name": "in_array", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -24510,6 +25374,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -24518,7 +25383,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.inet_ntop` through `BuiltinLoweringContext`.", @@ -24530,6 +25395,8 @@ "sig_line": null }, "name": "inet_ntop", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -24608,6 +25475,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -24616,7 +25484,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.inet_pton` through `BuiltinLoweringContext`.", @@ -24628,6 +25496,8 @@ "sig_line": null }, "name": "inet_pton", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -24712,6 +25582,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -24720,7 +25591,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.intdiv` through `BuiltinLoweringContext`.", @@ -24732,6 +25603,8 @@ "sig_line": null }, "name": "intdiv", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -24839,6 +25712,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -24847,7 +25721,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.interface_exists` through `BuiltinLoweringContext`.", @@ -24859,6 +25733,8 @@ "sig_line": null }, "name": "interface_exists", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -24961,6 +25837,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -24969,7 +25846,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -24980,6 +25857,8 @@ "sig_line": null }, "name": "intval", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -25052,6 +25931,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -25060,7 +25940,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ip2long` through `BuiltinLoweringContext`.", @@ -25072,6 +25952,8 @@ "sig_line": null }, "name": "ip2long", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -25162,6 +26044,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -25170,7 +26053,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_a` through `BuiltinLoweringContext`.", @@ -25182,6 +26065,8 @@ "sig_line": null }, "name": "is_a", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -25290,6 +26175,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -25298,7 +26184,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -25309,6 +26195,8 @@ "sig_line": null }, "name": "is_array", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -25381,6 +26269,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -25389,7 +26278,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -25400,6 +26289,8 @@ "sig_line": null }, "name": "is_bool", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -25484,6 +26375,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -25492,7 +26384,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_callable` through `BuiltinLoweringContext`.", @@ -25504,6 +26396,8 @@ "sig_line": null }, "name": "is_callable", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -25598,6 +26492,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -25606,7 +26501,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_dir` through `BuiltinLoweringContext`.", @@ -25618,6 +26513,8 @@ "sig_line": null }, "name": "is_dir", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -25712,6 +26609,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -25720,7 +26618,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -25731,6 +26629,8 @@ "sig_line": null }, "name": "is_double", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -25803,6 +26703,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -25811,7 +26712,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_executable` through `BuiltinLoweringContext`.", @@ -25823,6 +26724,8 @@ "sig_line": null }, "name": "is_executable", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -25917,6 +26820,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -25925,7 +26829,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_file` through `BuiltinLoweringContext`.", @@ -25937,6 +26841,8 @@ "sig_line": null }, "name": "is_file", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -26031,6 +26937,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -26039,7 +26946,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_finite` through `BuiltinLoweringContext`.", @@ -26051,6 +26958,8 @@ "sig_line": null }, "name": "is_finite", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -26145,6 +27054,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -26153,7 +27063,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -26164,6 +27074,8 @@ "sig_line": null }, "name": "is_float", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -26236,6 +27148,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -26244,7 +27157,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_infinite` through `BuiltinLoweringContext`.", @@ -26256,6 +27169,8 @@ "sig_line": null }, "name": "is_infinite", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -26350,6 +27265,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -26358,7 +27274,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -26369,6 +27285,8 @@ "sig_line": null }, "name": "is_int", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -26441,6 +27359,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -26449,7 +27368,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -26460,6 +27379,8 @@ "sig_line": null }, "name": "is_integer", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -26532,6 +27453,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -26540,7 +27462,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -26551,6 +27473,8 @@ "sig_line": null }, "name": "is_iterable", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -26623,6 +27547,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -26631,7 +27556,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_link` through `BuiltinLoweringContext`.", @@ -26643,6 +27568,8 @@ "sig_line": null }, "name": "is_link", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -26737,6 +27664,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -26745,7 +27673,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -26756,6 +27684,8 @@ "sig_line": null }, "name": "is_long", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -26828,6 +27758,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -26836,7 +27767,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_nan` through `BuiltinLoweringContext`.", @@ -26848,6 +27779,8 @@ "sig_line": null }, "name": "is_nan", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -26942,6 +27875,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -26950,7 +27884,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -26961,6 +27895,8 @@ "sig_line": null }, "name": "is_null", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -27034,6 +27970,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -27042,7 +27979,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_numeric` through `BuiltinLoweringContext`.", @@ -27054,6 +27991,8 @@ "sig_line": null }, "name": "is_numeric", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -27131,6 +28070,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -27139,7 +28079,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -27150,6 +28090,8 @@ "sig_line": null }, "name": "is_object", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -27222,6 +28164,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -27230,7 +28173,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_readable` through `BuiltinLoweringContext`.", @@ -27242,6 +28185,8 @@ "sig_line": null }, "name": "is_readable", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -27336,6 +28281,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -27344,7 +28290,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -27355,6 +28301,8 @@ "sig_line": null }, "name": "is_real", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -27427,6 +28375,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -27435,7 +28384,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -27446,6 +28395,8 @@ "sig_line": null }, "name": "is_resource", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -27518,6 +28469,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -27526,7 +28478,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -27537,6 +28489,8 @@ "sig_line": null }, "name": "is_scalar", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -27609,6 +28563,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -27617,7 +28572,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -27628,6 +28583,8 @@ "sig_line": null }, "name": "is_string", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -27712,6 +28669,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -27720,7 +28678,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_subclass_of` through `BuiltinLoweringContext`.", @@ -27732,6 +28690,8 @@ "sig_line": null }, "name": "is_subclass_of", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -27840,6 +28800,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -27848,7 +28809,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_writable` through `BuiltinLoweringContext`.", @@ -27860,6 +28821,8 @@ "sig_line": null }, "name": "is_writable", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -27954,6 +28917,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -27962,7 +28926,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_writeable` through `BuiltinLoweringContext`.", @@ -27974,6 +28938,8 @@ "sig_line": null }, "name": "is_writeable", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -28068,6 +29034,7 @@ "variadic": "vars" }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -28086,6 +29053,8 @@ "sig_line": null }, "name": "isset", + "notes": [], + "see_also": [], "semantics": null, "sig": { "params": [ @@ -28140,6 +29109,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -28148,7 +29118,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.iterator_apply` through `BuiltinLoweringContext`.", @@ -28160,6 +29130,8 @@ "sig_line": null }, "name": "iterator_apply", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -28269,6 +29241,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -28277,7 +29250,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.iterator_count` through `BuiltinLoweringContext`.", @@ -28289,6 +29262,8 @@ "sig_line": null }, "name": "iterator_count", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -28390,6 +29365,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -28398,7 +29374,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.iterator_to_array` through `BuiltinLoweringContext`.", @@ -28410,6 +29386,8 @@ "sig_line": null }, "name": "iterator_to_array", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -28530,6 +29508,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -28538,7 +29517,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.json_decode` through `BuiltinLoweringContext`.", @@ -28550,6 +29529,8 @@ "sig_line": null }, "name": "json_decode", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "json_decode", "callable": { @@ -28678,6 +29659,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -28686,7 +29668,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.json_encode` through `BuiltinLoweringContext`.", @@ -28698,6 +29680,8 @@ "sig_line": null }, "name": "json_encode", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -28800,6 +29784,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -28808,7 +29793,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.json_last_error` through `BuiltinLoweringContext`.", @@ -28820,6 +29805,8 @@ "sig_line": null }, "name": "json_last_error", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -28899,6 +29886,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -28907,7 +29895,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.json_last_error_msg` through `BuiltinLoweringContext`.", @@ -28919,6 +29907,8 @@ "sig_line": null }, "name": "json_last_error_msg", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -29017,6 +30007,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -29025,7 +30016,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.json_validate` through `BuiltinLoweringContext`.", @@ -29037,6 +30028,8 @@ "sig_line": null }, "name": "json_validate", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -29145,6 +30138,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -29153,7 +30147,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.krsort` through `BuiltinLoweringContext`.", @@ -29165,6 +30159,8 @@ "sig_line": null }, "name": "krsort", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -29259,6 +30255,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -29267,7 +30264,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ksort` through `BuiltinLoweringContext`.", @@ -29279,6 +30276,8 @@ "sig_line": null }, "name": "ksort", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -29374,6 +30373,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -29382,7 +30382,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.lcfirst` through `BuiltinLoweringContext`.", @@ -29394,6 +30394,8 @@ "sig_line": null }, "name": "lcfirst", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -29477,6 +30479,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -29485,7 +30488,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.lchgrp` through `BuiltinLoweringContext`.", @@ -29497,6 +30500,8 @@ "sig_line": null }, "name": "lchgrp", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -29565,7 +30570,7 @@ "default": null, "name": "group", "optional": false, - "type": "string" + "type": "mixed" } ], "return_type": "bool", @@ -29605,6 +30610,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -29613,7 +30619,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.lchown` through `BuiltinLoweringContext`.", @@ -29625,6 +30631,8 @@ "sig_line": null }, "name": "lchown", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -29693,7 +30701,7 @@ "default": null, "name": "user", "optional": false, - "type": "string" + "type": "mixed" } ], "return_type": "bool", @@ -29733,6 +30741,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -29741,7 +30750,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.link` through `BuiltinLoweringContext`.", @@ -29753,6 +30762,8 @@ "sig_line": null }, "name": "link", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -29854,6 +30865,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -29862,7 +30874,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.linkinfo` through `BuiltinLoweringContext`.", @@ -29874,6 +30886,8 @@ "sig_line": null }, "name": "linkinfo", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -29974,6 +30988,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -29982,7 +30997,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.localtime` through `BuiltinLoweringContext`.", @@ -29994,6 +31009,8 @@ "sig_line": null }, "name": "localtime", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -30101,6 +31118,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -30109,7 +31127,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.log` through `BuiltinLoweringContext`.", @@ -30121,6 +31139,8 @@ "sig_line": null }, "name": "log", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -30205,6 +31225,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -30213,7 +31234,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.log10` through `BuiltinLoweringContext`.", @@ -30225,6 +31246,8 @@ "sig_line": null }, "name": "log10", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -30302,6 +31325,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -30310,7 +31334,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.log2` through `BuiltinLoweringContext`.", @@ -30322,6 +31346,8 @@ "sig_line": null }, "name": "log2", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -30399,6 +31425,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -30407,7 +31434,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.long2ip` through `BuiltinLoweringContext`.", @@ -30419,6 +31446,8 @@ "sig_line": null }, "name": "long2ip", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -30496,6 +31525,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -30504,7 +31534,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.lstat` through `BuiltinLoweringContext`.", @@ -30516,6 +31546,8 @@ "sig_line": null }, "name": "lstat", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -30617,6 +31649,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -30625,7 +31658,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ltrim` through `BuiltinLoweringContext`.", @@ -30637,6 +31670,8 @@ "sig_line": null }, "name": "ltrim", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -30721,6 +31756,7 @@ "variadic": "values" }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -30729,7 +31765,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.max` through `BuiltinLoweringContext`.", @@ -30741,6 +31777,8 @@ "sig_line": null }, "name": "max", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -30831,6 +31869,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -30839,7 +31878,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.mb_ereg_match` through `BuiltinLoweringContext`.", @@ -30853,6 +31892,8 @@ "sig_line": null }, "name": "mb_ereg_match", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -30968,6 +32009,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -30976,7 +32018,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.mb_strlen` through `BuiltinLoweringContext`.", @@ -30988,6 +32030,8 @@ "sig_line": null }, "name": "mb_strlen", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -31029,6 +32073,10 @@ { "kind": "macos_library", "name": "iconv" + }, + { + "kind": "windows_library", + "name": "iconv" } ] }, @@ -31061,7 +32109,7 @@ "default": "null", "name": "encoding", "optional": true, - "type": "string" + "type": "mixed" } ], "return_type": "int", @@ -31101,6 +32149,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -31109,7 +32158,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.md5` through `BuiltinLoweringContext`.", @@ -31121,6 +32170,8 @@ "sig_line": null }, "name": "md5", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -31216,6 +32267,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -31224,7 +32276,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.method_exists` through `BuiltinLoweringContext`.", @@ -31236,6 +32288,8 @@ "sig_line": null }, "name": "method_exists", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -31337,6 +32391,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -31345,7 +32400,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.microtime` through `BuiltinLoweringContext`.", @@ -31357,6 +32412,8 @@ "sig_line": null }, "name": "microtime", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -31452,6 +32509,7 @@ "variadic": "values" }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -31460,7 +32518,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.min` through `BuiltinLoweringContext`.", @@ -31472,6 +32530,8 @@ "sig_line": null }, "name": "min", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -31550,6 +32610,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -31558,7 +32619,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.mkdir` through `BuiltinLoweringContext`.", @@ -31570,6 +32631,8 @@ "sig_line": null }, "name": "mkdir", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -31694,6 +32757,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -31702,7 +32766,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.mktime` through `BuiltinLoweringContext`.", @@ -31714,6 +32778,8 @@ "sig_line": null }, "name": "mktime", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -31750,8 +32816,7 @@ "kind": "may_alias_arguments" }, "requirements": { - "kind": "static", - "values": [] + "kind": "shared" }, "result_type": "declared", "runtime_functions": [ @@ -31849,6 +32914,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -31857,7 +32923,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.mt_rand` through `BuiltinLoweringContext`.", @@ -31869,6 +32935,8 @@ "sig_line": null }, "name": "mt_rand", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -31970,6 +33038,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -31978,7 +33047,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.natcasesort` through `BuiltinLoweringContext`.", @@ -31990,6 +33059,8 @@ "sig_line": null }, "name": "natcasesort", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -32084,6 +33155,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -32092,7 +33164,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.natsort` through `BuiltinLoweringContext`.", @@ -32104,6 +33176,8 @@ "sig_line": null }, "name": "natsort", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -32205,6 +33279,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -32213,7 +33288,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.string.nl_to_br` through `BuiltinLoweringContext`.", @@ -32225,6 +33300,8 @@ "sig_line": null }, "name": "nl2br", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -32317,6 +33394,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -32325,7 +33403,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.number_format` through `BuiltinLoweringContext`.", @@ -32337,6 +33415,8 @@ "sig_line": null }, "name": "number_format", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -32428,6 +33508,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -32436,7 +33517,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ob_clean` through `BuiltinLoweringContext`.", @@ -32448,6 +33529,8 @@ "sig_line": null }, "name": "ob_clean", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -32527,6 +33610,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -32535,7 +33619,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ob_end_clean` through `BuiltinLoweringContext`.", @@ -32547,6 +33631,8 @@ "sig_line": null }, "name": "ob_end_clean", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -32626,6 +33712,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -32634,7 +33721,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ob_end_flush` through `BuiltinLoweringContext`.", @@ -32646,6 +33733,8 @@ "sig_line": null }, "name": "ob_end_flush", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -32725,6 +33814,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -32733,7 +33823,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ob_flush` through `BuiltinLoweringContext`.", @@ -32745,6 +33835,8 @@ "sig_line": null }, "name": "ob_flush", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -32824,6 +33916,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -32832,7 +33925,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ob_get_clean` through `BuiltinLoweringContext`.", @@ -32844,6 +33937,8 @@ "sig_line": null }, "name": "ob_get_clean", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -32924,6 +34019,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -32932,7 +34028,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ob_get_contents` through `BuiltinLoweringContext`.", @@ -32944,6 +34040,8 @@ "sig_line": null }, "name": "ob_get_contents", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -33024,6 +34122,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -33032,7 +34131,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ob_get_flush` through `BuiltinLoweringContext`.", @@ -33044,6 +34143,8 @@ "sig_line": null }, "name": "ob_get_flush", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -33124,6 +34225,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -33132,7 +34234,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ob_get_length` through `BuiltinLoweringContext`.", @@ -33144,6 +34246,8 @@ "sig_line": null }, "name": "ob_get_length", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -33224,6 +34328,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -33232,7 +34337,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ob_get_level` through `BuiltinLoweringContext`.", @@ -33244,6 +34349,8 @@ "sig_line": null }, "name": "ob_get_level", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -33330,6 +34437,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -33338,7 +34446,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ob_get_status` through `BuiltinLoweringContext`.", @@ -33350,6 +34458,8 @@ "sig_line": null }, "name": "ob_get_status", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -33445,6 +34555,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -33453,7 +34564,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ob_implicit_flush` through `BuiltinLoweringContext`.", @@ -33465,6 +34576,8 @@ "sig_line": null }, "name": "ob_implicit_flush", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -33552,6 +34665,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -33560,7 +34674,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ob_list_handlers` through `BuiltinLoweringContext`.", @@ -33572,6 +34686,8 @@ "sig_line": null }, "name": "ob_list_handlers", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -33671,6 +34787,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -33679,7 +34796,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ob_start` through `BuiltinLoweringContext`.", @@ -33691,6 +34808,8 @@ "sig_line": null }, "name": "ob_start", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -33800,6 +34919,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -33808,7 +34928,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.opendir` through `BuiltinLoweringContext`.", @@ -33820,6 +34940,8 @@ "sig_line": null }, "name": "opendir", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -33915,6 +35037,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -33923,7 +35046,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ord` through `BuiltinLoweringContext`.", @@ -33935,6 +35058,8 @@ "sig_line": null }, "name": "ord", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -34012,6 +35137,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -34020,7 +35146,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.passthru` through `BuiltinLoweringContext`.", @@ -34032,6 +35158,8 @@ "sig_line": null }, "name": "passthru", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -34132,6 +35260,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -34140,7 +35269,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.pathinfo` through `BuiltinLoweringContext`.", @@ -34152,6 +35281,8 @@ "sig_line": null }, "name": "pathinfo", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -34254,6 +35385,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -34262,7 +35394,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.pclose` through `BuiltinLoweringContext`.", @@ -34274,6 +35406,8 @@ "sig_line": null }, "name": "pclose", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -34392,6 +35526,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -34400,7 +35535,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.pfsockopen` through `BuiltinLoweringContext`.", @@ -34412,6 +35547,8 @@ "sig_line": null }, "name": "pfsockopen", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -34535,6 +35672,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -34543,7 +35681,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.php_uname` through `BuiltinLoweringContext`.", @@ -34555,6 +35693,8 @@ "sig_line": null }, "name": "php_uname", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -34643,6 +35783,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -34651,7 +35792,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.phpversion` through `BuiltinLoweringContext`.", @@ -34663,6 +35804,8 @@ "sig_line": null }, "name": "phpversion", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -34742,6 +35885,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -34750,7 +35894,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.pi` through `BuiltinLoweringContext`.", @@ -34762,6 +35906,8 @@ "sig_line": null }, "name": "pi", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -34837,6 +35983,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -34845,7 +35992,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.popen` through `BuiltinLoweringContext`.", @@ -34857,6 +36004,8 @@ "sig_line": null }, "name": "popen", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -34965,6 +36114,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -34973,7 +36123,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.pow` through `BuiltinLoweringContext`.", @@ -34985,6 +36135,8 @@ "sig_line": null }, "name": "pow", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -35087,6 +36239,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -35095,7 +36248,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.preg_match` through `BuiltinLoweringContext`.", @@ -35107,6 +36260,8 @@ "sig_line": null }, "name": "preg_match", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "positional_regex", "callable": { @@ -35234,6 +36389,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -35242,7 +36398,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.preg_match_all` through `BuiltinLoweringContext`.", @@ -35254,6 +36410,8 @@ "sig_line": null }, "name": "preg_match_all", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -35367,6 +36525,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -35375,7 +36534,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.preg_replace` through `BuiltinLoweringContext`.", @@ -35387,6 +36546,8 @@ "sig_line": null }, "name": "preg_replace", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -35507,6 +36668,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -35515,7 +36677,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.preg_replace_callback` through `BuiltinLoweringContext`.", @@ -35527,6 +36689,8 @@ "sig_line": null }, "name": "preg_replace_callback", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "preg_replace_callback", "callable": { @@ -35654,6 +36818,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -35662,7 +36827,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.preg_split` through `BuiltinLoweringContext`.", @@ -35674,6 +36839,8 @@ "sig_line": null }, "name": "preg_split", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "positional_regex", "callable": { @@ -35796,6 +36963,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -35804,7 +36972,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.print_r` through `BuiltinLoweringContext`.", @@ -35816,6 +36984,8 @@ "sig_line": null }, "name": "print_r", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -35918,6 +37088,7 @@ "variadic": "values" }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -35926,7 +37097,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.printf` through `BuiltinLoweringContext`.", @@ -35938,6 +37109,8 @@ "sig_line": null }, "name": "printf", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -36008,12 +37181,12 @@ "sub_area": "String" }, { - "area": "Class", - "canonical_name": "property_exists", - "description": "Checks whether an object or class has a property.", + "area": "Process", + "canonical_name": "proc_close", + "description": "Close a process opened by proc_open and return the exit status.", "eval": { - "area": "symbols", - "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/property_exists.rs", + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/proc_close.rs", "hooks": [ "direct", "values" @@ -36023,21 +37196,16 @@ { "by_ref": false, "default": null, - "name": "object_or_class", - "optional": false - }, - { - "by_ref": false, - "default": null, - "name": "property", + "name": "process", "optional": false } ], - "required_param_count": 2, + "required_param_count": 1, "supported": true, "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -36046,18 +37214,20 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.property_exists` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.proc_close` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/callables/property_exists.rs", + "sig_file": "src/builtins/io/proc_close.rs", "sig_line": null }, - "name": "property_exists", + "name": "proc_close", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -36087,7 +37257,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "property_exists" + "target": "proc_close" }, "ownership": { "argument_indexes": [], @@ -36099,7 +37269,7 @@ }, "result_type": "declared", "runtime_functions": [ - "property_exists" + "proc_close" ], "target_strategy": "runtime_call", "target_support": [ @@ -36116,31 +37286,24 @@ { "by_ref": false, "default": null, - "name": "object_or_class", - "optional": false, - "type": "mixed" - }, - { - "by_ref": false, - "default": null, - "name": "property", + "name": "process", "optional": false, - "type": "string" + "type": "resource" } ], - "return_type": "bool", + "return_type": "int", "variadic": null }, - "slug": "property_exists", - "sub_area": "Class" + "slug": "proc_close", + "sub_area": "Process" }, { - "area": "Pointer", - "canonical_name": "ptr", - "description": "Returns a raw pointer to the given variable.", + "area": "Process", + "canonical_name": "proc_get_status", + "description": "Retrieves the current status of a process opened by proc_open.", "eval": { - "area": "raw_memory", - "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr.rs", + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/proc_get_status.rs", "hooks": [ "direct", "values" @@ -36150,7 +37313,7 @@ { "by_ref": false, "default": null, - "name": "value", + "name": "process", "optional": false } ], @@ -36159,26 +37322,33 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, - "is_extension": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ptr` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.proc_get_status` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/pointers/ptr.rs", + "sig_file": "src/builtins/io/proc_get_status.rs", "sig_line": null }, - "name": "ptr", + "name": "proc_get_status", + "notes": [ + "The status record contains `command`, `pid`, `cached`, `running`, `signaled`, `stopped`, `exitcode`, `termsig`, and `stopsig`.", + "Windows status queries do not reap the process and report `cached` as `false`.", + "Unix caches a normally exited child so `proc_close()` can still return its exit code." + ], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -36208,7 +37378,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ptr" + "target": "proc_get_status" }, "ownership": { "argument_indexes": [], @@ -36220,7 +37390,7 @@ }, "result_type": "checked", "runtime_functions": [ - "ptr" + "proc_get_status" ], "target_strategy": "runtime_call", "target_support": [ @@ -36238,26 +37408,25 @@ { "by_ref": false, "default": null, - "name": "value", + "name": "process", "optional": false, - "type": "mixed" + "type": "resource" } ], - "return_type": "mixed", + "return_type": "array|false", "variadic": null }, - "slug": "ptr", - "sub_area": "Pointer" + "slug": "proc_get_status", + "sub_area": "Process" }, { - "area": "Pointer", - "canonical_name": "ptr_get", - "description": "Reads one machine word through a raw pointer and returns it as an integer.", + "area": "Process", + "canonical_name": "proc_open", + "description": "Execute a command and open file pointers for process I/O.", "eval": { - "area": "raw_memory", - "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_get.rs", + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/proc_open.rs", "hooks": [ - "direct", "values" ], "kind": "registry", @@ -36265,37 +37434,77 @@ { "by_ref": false, "default": null, - "name": "pointer", + "name": "command", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "descriptor_spec", + "optional": false + }, + { + "by_ref": true, + "default": null, + "name": "pipes", "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "cwd", + "optional": true + }, + { + "by_ref": false, + "default": "null", + "name": "env_vars", + "optional": true + }, + { + "by_ref": false, + "default": "null", + "name": "options", + "optional": true } ], - "required_param_count": 1, + "required_param_count": 3, "supported": true, "variadic": null }, "eval_only": false, + "examples": [ + "See [`examples/process-pipes`](../../../../examples/process-pipes/main.php) for a complete stdin/stdout/stderr exchange and process-status query." + ], "in_catalog": true, - "is_extension": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ptr_get` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.proc_open` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/pointers/ptr_get.rs", + "sig_file": "src/builtins/io/proc_open.rs", "sig_line": null }, - "name": "ptr_get", + "name": "proc_open", + "notes": [ + "Windows accepts string and array commands, UTF-8 working directories and environment maps, plus PHP's documented process options.", + "The Windows descriptor runtime supports `pipe`, `socket`, `file`, `redirect`, stream-resource, and `null` entries while preserving sparse integer keys.", + "Descriptors above 2 are rejected on Windows because `STARTUPINFOW` cannot expose them as matching numbered CRT descriptors.", + "The Windows `blocking_pipes` option selects blocking `ReadFile` behavior; the default probes readable pipes and reports `EAGAIN` when no bytes are available." + ], + "see_also": [], "semantics": { - "argument_lowering": "standard", + "argument_lowering": "proc_open", "callable": { "kind": "static_only", "reason": "typed backend operation has no runtime-selected wrapper contract" @@ -36323,7 +37532,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ptr_get" + "target": "proc_open" }, "ownership": { "argument_indexes": [], @@ -36335,7 +37544,7 @@ }, "result_type": "checked", "runtime_functions": [ - "ptr_get" + "proc_open" ], "target_strategy": "runtime_call", "target_support": [ @@ -36353,24 +37562,59 @@ { "by_ref": false, "default": null, - "name": "pointer", + "name": "command", "optional": false, - "type": "pointer" + "type": "array|string" + }, + { + "by_ref": false, + "default": null, + "name": "descriptor_spec", + "optional": false, + "type": "array" + }, + { + "by_ref": true, + "default": null, + "name": "pipes", + "optional": false, + "type": "array" + }, + { + "by_ref": false, + "default": "null", + "name": "cwd", + "optional": true, + "type": "?string" + }, + { + "by_ref": false, + "default": "null", + "name": "env_vars", + "optional": true, + "type": "?array" + }, + { + "by_ref": false, + "default": "null", + "name": "options", + "optional": true, + "type": "?array" } ], - "return_type": "int", + "return_type": "resource|false", "variadic": null }, - "slug": "ptr_get", - "sub_area": "Pointer" + "slug": "proc_open", + "sub_area": "Process" }, { - "area": "Pointer", - "canonical_name": "ptr_is_null", - "description": "Returns true if the pointer is null.", + "area": "Process", + "canonical_name": "proc_terminate", + "description": "Terminates a process opened by proc_open.", "eval": { - "area": "raw_memory", - "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_is_null.rs", + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/proc_terminate.rs", "hooks": [ "direct", "values" @@ -36380,8 +37624,14 @@ { "by_ref": false, "default": null, - "name": "pointer", + "name": "process", "optional": false + }, + { + "by_ref": false, + "default": "15", + "name": "signal", + "optional": true } ], "required_param_count": 1, @@ -36389,26 +37639,32 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, - "is_extension": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ptr_is_null` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.proc_terminate` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/pointers/ptr_is_null.rs", + "sig_file": "src/builtins/io/proc_terminate.rs", "sig_line": null }, - "name": "ptr_is_null", + "name": "proc_terminate", + "notes": [ + "Unix forwards the optional signal to `kill(2)`.", + "Windows follows PHP by ignoring the signal value and terminating the process with exit code 255." + ], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -36438,7 +37694,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ptr_is_null" + "target": "proc_terminate" }, "ownership": { "argument_indexes": [], @@ -36450,7 +37706,7 @@ }, "result_type": "checked", "runtime_functions": [ - "ptr_is_null" + "proc_terminate" ], "target_strategy": "runtime_call", "target_support": [ @@ -36468,55 +37724,78 @@ { "by_ref": false, "default": null, - "name": "pointer", + "name": "process", "optional": false, - "type": "pointer" + "type": "resource" + }, + { + "by_ref": false, + "default": "15", + "name": "signal", + "optional": true, + "type": "int" } ], "return_type": "bool", "variadic": null }, - "slug": "ptr_is_null", - "sub_area": "Pointer" + "slug": "proc_terminate", + "sub_area": "Process" }, { - "area": "Pointer", - "canonical_name": "ptr_null", - "description": "Returns a null raw pointer.", + "area": "Class", + "canonical_name": "property_exists", + "description": "Checks whether an object or class has a property.", "eval": { - "area": "raw_memory", - "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_null.rs", + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/property_exists.rs", "hooks": [ "direct", "values" ], "kind": "registry", - "params": [], - "required_param_count": 0, + "params": [ + { + "by_ref": false, + "default": null, + "name": "object_or_class", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "property", + "optional": false + } + ], + "required_param_count": 2, "supported": true, "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, - "is_extension": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ptr_null` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.property_exists` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/pointers/ptr_null.rs", + "sig_file": "src/builtins/callables/property_exists.rs", "sig_line": null }, - "name": "ptr_null", + "name": "property_exists", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -36546,7 +37825,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ptr_null" + "target": "property_exists" }, "ownership": { "argument_indexes": [], @@ -36556,9 +37835,9 @@ "kind": "static", "values": [] }, - "result_type": "checked", + "result_type": "declared", "runtime_functions": [ - "ptr_null" + "property_exists" ], "target_strategy": "runtime_call", "target_support": [ @@ -36567,25 +37846,39 @@ "linux-x86_64" ], "validation": { - "kind": "checker_hook", - "lazy": false + "kind": "signature" } }, "sig": { - "params": [], - "return_type": "mixed", + "params": [ + { + "by_ref": false, + "default": null, + "name": "object_or_class", + "optional": false, + "type": "mixed" + }, + { + "by_ref": false, + "default": null, + "name": "property", + "optional": false, + "type": "string" + } + ], + "return_type": "bool", "variadic": null }, - "slug": "ptr_null", - "sub_area": "Pointer" + "slug": "property_exists", + "sub_area": "Class" }, { "area": "Pointer", - "canonical_name": "ptr_offset", - "description": "Returns a new pointer offset from the given pointer by the given byte count.", + "canonical_name": "ptr", + "description": "Returns a raw pointer to the given variable.", "eval": { "area": "raw_memory", - "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_offset.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr.rs", "hooks": [ "direct", "values" @@ -36595,21 +37888,16 @@ { "by_ref": false, "default": null, - "name": "pointer", - "optional": false - }, - { - "by_ref": false, - "default": null, - "name": "offset", + "name": "value", "optional": false } ], - "required_param_count": 2, + "required_param_count": 1, "supported": true, "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": true, "is_internal": false, @@ -36618,18 +37906,20 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ptr_offset` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ptr` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/pointers/ptr_offset.rs", + "sig_file": "src/builtins/pointers/ptr.rs", "sig_line": null }, - "name": "ptr_offset", + "name": "ptr", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -36659,7 +37949,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ptr_offset" + "target": "ptr" }, "ownership": { "argument_indexes": [], @@ -36671,7 +37961,7 @@ }, "result_type": "checked", "runtime_functions": [ - "ptr_offset" + "ptr" ], "target_strategy": "runtime_call", "target_support": [ @@ -36689,31 +37979,24 @@ { "by_ref": false, "default": null, - "name": "pointer", - "optional": false, - "type": "pointer" - }, - { - "by_ref": false, - "default": null, - "name": "offset", + "name": "value", "optional": false, - "type": "int" + "type": "mixed" } ], "return_type": "mixed", "variadic": null }, - "slug": "ptr_offset", + "slug": "ptr", "sub_area": "Pointer" }, { "area": "Pointer", - "canonical_name": "ptr_read16", - "description": "Reads one unsigned 16-bit word through a raw pointer and returns it as an integer.", + "canonical_name": "ptr_get", + "description": "Reads one machine word through a raw pointer and returns it as an integer.", "eval": { "area": "raw_memory", - "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read16.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_get.rs", "hooks": [ "direct", "values" @@ -36732,6 +38015,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": true, "is_internal": false, @@ -36740,18 +38024,20 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ptr_read16` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ptr_get` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/pointers/ptr_read16.rs", + "sig_file": "src/builtins/pointers/ptr_get.rs", "sig_line": null }, - "name": "ptr_read16", + "name": "ptr_get", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -36781,7 +38067,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ptr_read16" + "target": "ptr_get" }, "ownership": { "argument_indexes": [], @@ -36793,7 +38079,7 @@ }, "result_type": "checked", "runtime_functions": [ - "ptr_read16" + "ptr_get" ], "target_strategy": "runtime_call", "target_support": [ @@ -36819,16 +38105,16 @@ "return_type": "int", "variadic": null }, - "slug": "ptr_read16", + "slug": "ptr_get", "sub_area": "Pointer" }, { "area": "Pointer", - "canonical_name": "ptr_read32", - "description": "Reads one unsigned 32-bit word through a raw pointer and returns it as an integer.", + "canonical_name": "ptr_is_null", + "description": "Returns true if the pointer is null.", "eval": { "area": "raw_memory", - "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read32.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_is_null.rs", "hooks": [ "direct", "values" @@ -36847,6 +38133,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": true, "is_internal": false, @@ -36855,18 +38142,20 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ptr_read32` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ptr_is_null` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/pointers/ptr_read32.rs", + "sig_file": "src/builtins/pointers/ptr_is_null.rs", "sig_line": null }, - "name": "ptr_read32", + "name": "ptr_is_null", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -36896,7 +38185,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ptr_read32" + "target": "ptr_is_null" }, "ownership": { "argument_indexes": [], @@ -36908,7 +38197,7 @@ }, "result_type": "checked", "runtime_functions": [ - "ptr_read32" + "ptr_is_null" ], "target_strategy": "runtime_call", "target_support": [ @@ -36931,19 +38220,122 @@ "type": "pointer" } ], - "return_type": "int", + "return_type": "bool", "variadic": null }, - "slug": "ptr_read32", + "slug": "ptr_is_null", "sub_area": "Pointer" }, { "area": "Pointer", - "canonical_name": "ptr_read8", - "description": "Reads one unsigned byte through a raw pointer and returns it as an integer.", + "canonical_name": "ptr_null", + "description": "Returns a null raw pointer.", "eval": { "area": "raw_memory", - "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read8.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_null.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, + "examples": [], + "in_catalog": true, + "is_extension": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 426, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.ptr_null` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/pointers/ptr_null.rs", + "sig_line": null + }, + "name": "ptr_null", + "notes": [], + "see_also": [], + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "reads_local", + "writes_local", + "reads_heap", + "writes_heap", + "reads_global", + "reads_fs", + "writes_fs", + "reads_process", + "writes_process", + "output", + "alloc_heap", + "alloc_concat", + "may_throw", + "may_fatal", + "may_warn", + "may_deopt" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "ptr_null" + }, + "ownership": { + "argument_indexes": [], + "kind": "may_alias_arguments" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "checked", + "runtime_functions": [ + "ptr_null" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "checker_hook", + "lazy": false + } + }, + "sig": { + "params": [], + "return_type": "mixed", + "variadic": null + }, + "slug": "ptr_null", + "sub_area": "Pointer" + }, + { + "area": "Pointer", + "canonical_name": "ptr_offset", + "description": "Returns a new pointer offset from the given pointer by the given byte count.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_offset.rs", "hooks": [ "direct", "values" @@ -36955,13 +38347,20 @@ "default": null, "name": "pointer", "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "offset", + "optional": false } ], - "required_param_count": 1, + "required_param_count": 2, "supported": true, "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": true, "is_internal": false, @@ -36970,18 +38369,20 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ptr_read8` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ptr_offset` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/pointers/ptr_read8.rs", + "sig_file": "src/builtins/pointers/ptr_offset.rs", "sig_line": null }, - "name": "ptr_read8", + "name": "ptr_offset", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -37011,7 +38412,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ptr_read8" + "target": "ptr_offset" }, "ownership": { "argument_indexes": [], @@ -37023,7 +38424,7 @@ }, "result_type": "checked", "runtime_functions": [ - "ptr_read8" + "ptr_offset" ], "target_strategy": "runtime_call", "target_support": [ @@ -37044,21 +38445,28 @@ "name": "pointer", "optional": false, "type": "pointer" + }, + { + "by_ref": false, + "default": null, + "name": "offset", + "optional": false, + "type": "int" } ], - "return_type": "int", + "return_type": "mixed", "variadic": null }, - "slug": "ptr_read8", + "slug": "ptr_offset", "sub_area": "Pointer" }, { "area": "Pointer", - "canonical_name": "ptr_read_string", - "description": "Copies raw bytes from a pointer into a PHP string of the given length.", + "canonical_name": "ptr_read16", + "description": "Reads one unsigned 16-bit word through a raw pointer and returns it as an integer.", "eval": { "area": "raw_memory", - "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read_string.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read16.rs", "hooks": [ "direct", "values" @@ -37070,19 +38478,14 @@ "default": null, "name": "pointer", "optional": false - }, - { - "by_ref": false, - "default": null, - "name": "length", - "optional": false } ], - "required_param_count": 2, + "required_param_count": 1, "supported": true, "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": true, "is_internal": false, @@ -37091,18 +38494,20 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ptr_read_string` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ptr_read16` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/pointers/ptr_read_string.rs", + "sig_file": "src/builtins/pointers/ptr_read16.rs", "sig_line": null }, - "name": "ptr_read_string", + "name": "ptr_read16", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -37132,11 +38537,11 @@ }, "lowering": { "kind": "runtime_call", - "target": "ptr_read_string" + "target": "ptr_read16" }, "ownership": { "argument_indexes": [], - "kind": "fresh" + "kind": "may_alias_arguments" }, "requirements": { "kind": "static", @@ -37144,7 +38549,7 @@ }, "result_type": "checked", "runtime_functions": [ - "ptr_read_string" + "ptr_read16" ], "target_strategy": "runtime_call", "target_support": [ @@ -37165,28 +38570,21 @@ "name": "pointer", "optional": false, "type": "pointer" - }, - { - "by_ref": false, - "default": null, - "name": "length", - "optional": false, - "type": "int" } ], - "return_type": "string", + "return_type": "int", "variadic": null }, - "slug": "ptr_read_string", + "slug": "ptr_read16", "sub_area": "Pointer" }, { "area": "Pointer", - "canonical_name": "ptr_set", - "description": "Writes one machine word through a raw pointer.", + "canonical_name": "ptr_read32", + "description": "Reads one unsigned 32-bit word through a raw pointer and returns it as an integer.", "eval": { "area": "raw_memory", - "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_set.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read32.rs", "hooks": [ "direct", "values" @@ -37198,19 +38596,14 @@ "default": null, "name": "pointer", "optional": false - }, - { - "by_ref": false, - "default": null, - "name": "value", - "optional": false } ], - "required_param_count": 2, + "required_param_count": 1, "supported": true, "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": true, "is_internal": false, @@ -37219,18 +38612,20 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ptr_set` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ptr_read32` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/pointers/ptr_set.rs", + "sig_file": "src/builtins/pointers/ptr_read32.rs", "sig_line": null }, - "name": "ptr_set", + "name": "ptr_read32", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -37260,7 +38655,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ptr_set" + "target": "ptr_read32" }, "ownership": { "argument_indexes": [], @@ -37272,7 +38667,7 @@ }, "result_type": "checked", "runtime_functions": [ - "ptr_set" + "ptr_read32" ], "target_strategy": "runtime_call", "target_support": [ @@ -37293,28 +38688,21 @@ "name": "pointer", "optional": false, "type": "pointer" - }, - { - "by_ref": false, - "default": null, - "name": "value", - "optional": false, - "type": "mixed" } ], - "return_type": "void", + "return_type": "int", "variadic": null }, - "slug": "ptr_set", + "slug": "ptr_read32", "sub_area": "Pointer" }, { "area": "Pointer", - "canonical_name": "ptr_sizeof", - "description": "Returns the byte size of the named pointer target type.", + "canonical_name": "ptr_read8", + "description": "Reads one unsigned byte through a raw pointer and returns it as an integer.", "eval": { "area": "raw_memory", - "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_sizeof.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read8.rs", "hooks": [ "direct", "values" @@ -37324,7 +38712,7 @@ { "by_ref": false, "default": null, - "name": "type", + "name": "pointer", "optional": false } ], @@ -37333,6 +38721,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": true, "is_internal": false, @@ -37341,18 +38730,20 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ptr_sizeof` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ptr_read8` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/pointers/ptr_sizeof.rs", + "sig_file": "src/builtins/pointers/ptr_read8.rs", "sig_line": null }, - "name": "ptr_sizeof", + "name": "ptr_read8", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -37382,7 +38773,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ptr_sizeof" + "target": "ptr_read8" }, "ownership": { "argument_indexes": [], @@ -37394,7 +38785,7 @@ }, "result_type": "checked", "runtime_functions": [ - "ptr_sizeof" + "ptr_read8" ], "target_strategy": "runtime_call", "target_support": [ @@ -37412,24 +38803,24 @@ { "by_ref": false, "default": null, - "name": "type", + "name": "pointer", "optional": false, - "type": "string" + "type": "pointer" } ], "return_type": "int", "variadic": null }, - "slug": "ptr_sizeof", + "slug": "ptr_read8", "sub_area": "Pointer" }, { "area": "Pointer", - "canonical_name": "ptr_write16", - "description": "Writes one 16-bit word through a raw pointer.", + "canonical_name": "ptr_read_string", + "description": "Copies raw bytes from a pointer into a PHP string of the given length.", "eval": { "area": "raw_memory", - "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write16.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read_string.rs", "hooks": [ "direct", "values" @@ -37445,7 +38836,7 @@ { "by_ref": false, "default": null, - "name": "value", + "name": "length", "optional": false } ], @@ -37454,6 +38845,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": true, "is_internal": false, @@ -37462,18 +38854,20 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ptr_write16` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ptr_read_string` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/pointers/ptr_write16.rs", + "sig_file": "src/builtins/pointers/ptr_read_string.rs", "sig_line": null }, - "name": "ptr_write16", + "name": "ptr_read_string", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -37503,11 +38897,11 @@ }, "lowering": { "kind": "runtime_call", - "target": "ptr_write16" + "target": "ptr_read_string" }, "ownership": { "argument_indexes": [], - "kind": "may_alias_arguments" + "kind": "fresh" }, "requirements": { "kind": "static", @@ -37515,7 +38909,387 @@ }, "result_type": "checked", "runtime_functions": [ - "ptr_write16" + "ptr_read_string" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "checker_hook", + "lazy": false + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false, + "type": "pointer" + }, + { + "by_ref": false, + "default": null, + "name": "length", + "optional": false, + "type": "int" + } + ], + "return_type": "string", + "variadic": null + }, + "slug": "ptr_read_string", + "sub_area": "Pointer" + }, + { + "area": "Pointer", + "canonical_name": "ptr_set", + "description": "Writes one machine word through a raw pointer.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_set.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, + "examples": [], + "in_catalog": true, + "is_extension": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 426, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.ptr_set` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/pointers/ptr_set.rs", + "sig_line": null + }, + "name": "ptr_set", + "notes": [], + "see_also": [], + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "reads_local", + "writes_local", + "reads_heap", + "writes_heap", + "reads_global", + "reads_fs", + "writes_fs", + "reads_process", + "writes_process", + "output", + "alloc_heap", + "alloc_concat", + "may_throw", + "may_fatal", + "may_warn", + "may_deopt" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "ptr_set" + }, + "ownership": { + "argument_indexes": [], + "kind": "may_alias_arguments" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "checked", + "runtime_functions": [ + "ptr_set" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "checker_hook", + "lazy": false + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false, + "type": "pointer" + }, + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false, + "type": "mixed" + } + ], + "return_type": "void", + "variadic": null + }, + "slug": "ptr_set", + "sub_area": "Pointer" + }, + { + "area": "Pointer", + "canonical_name": "ptr_sizeof", + "description": "Returns the byte size of the named pointer target type.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_sizeof.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "type", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, + "examples": [], + "in_catalog": true, + "is_extension": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 426, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.ptr_sizeof` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/pointers/ptr_sizeof.rs", + "sig_line": null + }, + "name": "ptr_sizeof", + "notes": [], + "see_also": [], + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "reads_local", + "writes_local", + "reads_heap", + "writes_heap", + "reads_global", + "reads_fs", + "writes_fs", + "reads_process", + "writes_process", + "output", + "alloc_heap", + "alloc_concat", + "may_throw", + "may_fatal", + "may_warn", + "may_deopt" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "ptr_sizeof" + }, + "ownership": { + "argument_indexes": [], + "kind": "may_alias_arguments" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "checked", + "runtime_functions": [ + "ptr_sizeof" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "checker_hook", + "lazy": false + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "type", + "optional": false, + "type": "string" + } + ], + "return_type": "int", + "variadic": null + }, + "slug": "ptr_sizeof", + "sub_area": "Pointer" + }, + { + "area": "Pointer", + "canonical_name": "ptr_write16", + "description": "Writes one 16-bit word through a raw pointer.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write16.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, + "examples": [], + "in_catalog": true, + "is_extension": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 426, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.ptr_write16` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/pointers/ptr_write16.rs", + "sig_line": null + }, + "name": "ptr_write16", + "notes": [], + "see_also": [], + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "reads_local", + "writes_local", + "reads_heap", + "writes_heap", + "reads_global", + "reads_fs", + "writes_fs", + "reads_process", + "writes_process", + "output", + "alloc_heap", + "alloc_concat", + "may_throw", + "may_fatal", + "may_warn", + "may_deopt" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "ptr_write16" + }, + "ownership": { + "argument_indexes": [], + "kind": "may_alias_arguments" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "checked", + "runtime_functions": [ + "ptr_write16" ], "target_strategy": "runtime_call", "target_support": [ @@ -37582,6 +39356,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": true, "is_internal": false, @@ -37590,7 +39365,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ptr_write32` through `BuiltinLoweringContext`.", @@ -37602,6 +39377,8 @@ "sig_line": null }, "name": "ptr_write32", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -37710,6 +39487,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": true, "is_internal": false, @@ -37718,7 +39496,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ptr_write8` through `BuiltinLoweringContext`.", @@ -37730,6 +39508,8 @@ "sig_line": null }, "name": "ptr_write8", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -37838,6 +39618,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": true, "is_internal": false, @@ -37846,7 +39627,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ptr_write_string` through `BuiltinLoweringContext`.", @@ -37858,6 +39639,8 @@ "sig_line": null }, "name": "ptr_write_string", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -37960,6 +39743,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -37968,7 +39752,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.putenv` through `BuiltinLoweringContext`.", @@ -37980,6 +39764,8 @@ "sig_line": null }, "name": "putenv", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -38074,6 +39860,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -38082,7 +39869,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.rad2deg` through `BuiltinLoweringContext`.", @@ -38094,6 +39881,8 @@ "sig_line": null }, "name": "rad2deg", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -38177,6 +39966,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -38185,7 +39975,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.rand` through `BuiltinLoweringContext`.", @@ -38197,6 +39987,8 @@ "sig_line": null }, "name": "rand", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -38274,6 +40066,124 @@ "slug": "rand", "sub_area": "Math" }, + { + "area": "Math", + "canonical_name": "random_bytes", + "description": "Get a cryptographically secure random string of the given length.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/random_bytes.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "length", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, + "examples": [], + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 426, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.random_bytes` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/math/random_bytes.rs", + "sig_line": null + }, + "name": "random_bytes", + "notes": [], + "see_also": [], + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "reads_local", + "writes_local", + "reads_heap", + "writes_heap", + "reads_global", + "reads_fs", + "writes_fs", + "reads_process", + "writes_process", + "output", + "alloc_heap", + "alloc_concat", + "may_throw", + "may_fatal", + "may_warn", + "may_deopt" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "random_bytes" + }, + "ownership": { + "argument_indexes": [], + "kind": "may_alias_arguments" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "checked", + "runtime_functions": [ + "random_bytes" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "checker_hook", + "lazy": false + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "length", + "optional": false, + "type": "int" + } + ], + "return_type": "string", + "variadic": null + }, + "slug": "random_bytes", + "sub_area": "Math" + }, { "area": "Math", "canonical_name": "random_int", @@ -38305,6 +40215,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -38313,7 +40224,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.random_int` through `BuiltinLoweringContext`.", @@ -38325,6 +40236,8 @@ "sig_line": null }, "name": "random_int", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -38432,6 +40345,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -38440,7 +40354,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.range` through `BuiltinLoweringContext`.", @@ -38452,6 +40366,8 @@ "sig_line": null }, "name": "range", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -38537,6 +40453,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -38545,7 +40462,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.string.raw_url_decode` through `BuiltinLoweringContext`.", @@ -38557,6 +40474,8 @@ "sig_line": null }, "name": "rawurldecode", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -38631,6 +40550,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -38639,7 +40559,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.string.raw_url_encode` through `BuiltinLoweringContext`.", @@ -38651,6 +40571,8 @@ "sig_line": null }, "name": "rawurlencode", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -38725,6 +40647,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -38733,7 +40656,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.readdir` through `BuiltinLoweringContext`.", @@ -38745,6 +40668,8 @@ "sig_line": null }, "name": "readdir", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -38840,6 +40765,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -38848,7 +40774,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.readfile` through `BuiltinLoweringContext`.", @@ -38860,6 +40786,8 @@ "sig_line": null }, "name": "readfile", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -38955,6 +40883,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -38963,7 +40892,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.readline` through `BuiltinLoweringContext`.", @@ -38975,6 +40904,8 @@ "sig_line": null }, "name": "readline", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -39070,6 +41001,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -39078,7 +41010,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.readlink` through `BuiltinLoweringContext`.", @@ -39090,6 +41022,8 @@ "sig_line": null }, "name": "readlink", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -39185,6 +41119,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -39193,7 +41128,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.realpath` through `BuiltinLoweringContext`.", @@ -39205,6 +41140,8 @@ "sig_line": null }, "name": "realpath", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -39293,6 +41230,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -39301,7 +41239,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.realpath_cache_get` through `BuiltinLoweringContext`.", @@ -39313,6 +41251,8 @@ "sig_line": null }, "name": "realpath_cache_get", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -39393,6 +41333,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -39401,7 +41342,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.realpath_cache_size` through `BuiltinLoweringContext`.", @@ -39413,6 +41354,8 @@ "sig_line": null }, "name": "realpath_cache_size", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -39505,6 +41448,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -39513,7 +41457,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.rename` through `BuiltinLoweringContext`.", @@ -39525,6 +41469,8 @@ "sig_line": null }, "name": "rename", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -39626,6 +41572,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -39634,7 +41581,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.rewind` through `BuiltinLoweringContext`.", @@ -39646,6 +41593,8 @@ "sig_line": null }, "name": "rewind", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -39741,6 +41690,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -39749,7 +41699,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.rewinddir` through `BuiltinLoweringContext`.", @@ -39761,6 +41711,8 @@ "sig_line": null }, "name": "rewinddir", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -39856,6 +41808,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -39864,7 +41817,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.rmdir` through `BuiltinLoweringContext`.", @@ -39876,6 +41829,8 @@ "sig_line": null }, "name": "rmdir", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -39976,6 +41931,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -39984,7 +41940,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.round` through `BuiltinLoweringContext`.", @@ -39996,6 +41952,8 @@ "sig_line": null }, "name": "round", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -40079,6 +42037,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -40087,7 +42046,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.rsort` through `BuiltinLoweringContext`.", @@ -40099,6 +42058,8 @@ "sig_line": null }, "name": "rsort", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -40200,6 +42161,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -40208,7 +42170,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.rtrim` through `BuiltinLoweringContext`.", @@ -40220,6 +42182,8 @@ "sig_line": null }, "name": "rtrim", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -40304,6 +42268,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -40312,7 +42277,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.scandir` through `BuiltinLoweringContext`.", @@ -40324,6 +42289,8 @@ "sig_line": null }, "name": "scandir", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -40403,6 +42370,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -40411,7 +42379,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.serialize` through `BuiltinLoweringContext`.", @@ -40423,6 +42391,8 @@ "sig_line": null }, "name": "serialize", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -40522,6 +42492,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -40530,7 +42501,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.settype` through `BuiltinLoweringContext`.", @@ -40542,6 +42513,8 @@ "sig_line": null }, "name": "settype", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -40650,6 +42623,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -40658,7 +42632,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.sha1` through `BuiltinLoweringContext`.", @@ -40670,6 +42644,8 @@ "sig_line": null }, "name": "sha1", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -40759,6 +42735,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -40767,7 +42744,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.shell_exec` through `BuiltinLoweringContext`.", @@ -40779,6 +42756,8 @@ "sig_line": null }, "name": "shell_exec", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -40872,6 +42851,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -40880,7 +42860,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.shuffle` through `BuiltinLoweringContext`.", @@ -40892,6 +42872,8 @@ "sig_line": null }, "name": "shuffle", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -40987,6 +42969,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -40995,7 +42978,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.sin` through `BuiltinLoweringContext`.", @@ -41007,6 +42990,8 @@ "sig_line": null }, "name": "sin", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -41084,6 +43069,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -41092,7 +43078,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.sinh` through `BuiltinLoweringContext`.", @@ -41104,6 +43090,8 @@ "sig_line": null }, "name": "sinh", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -41181,6 +43169,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -41189,7 +43178,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.sleep` through `BuiltinLoweringContext`.", @@ -41201,6 +43190,8 @@ "sig_line": null }, "name": "sleep", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -41294,6 +43285,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -41302,7 +43294,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.sort` through `BuiltinLoweringContext`.", @@ -41314,6 +43306,8 @@ "sig_line": null }, "name": "sort", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -41415,6 +43409,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -41423,7 +43418,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.spl_autoload` through `BuiltinLoweringContext`.", @@ -41435,6 +43430,8 @@ "sig_line": null }, "name": "spl_autoload", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -41536,6 +43533,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -41544,7 +43542,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.spl_autoload_call` through `BuiltinLoweringContext`.", @@ -41556,6 +43554,8 @@ "sig_line": null }, "name": "spl_autoload_call", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -41650,6 +43650,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -41658,7 +43659,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.spl_autoload_extensions` through `BuiltinLoweringContext`.", @@ -41670,6 +43671,8 @@ "sig_line": null }, "name": "spl_autoload_extensions", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -41758,6 +43761,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -41766,7 +43770,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.spl_autoload_functions` through `BuiltinLoweringContext`.", @@ -41778,6 +43782,8 @@ "sig_line": null }, "name": "spl_autoload_functions", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -41877,6 +43883,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -41885,7 +43892,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.spl_autoload_register` through `BuiltinLoweringContext`.", @@ -41897,6 +43904,8 @@ "sig_line": null }, "name": "spl_autoload_register", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -42005,6 +44014,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -42013,7 +44023,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.spl_autoload_unregister` through `BuiltinLoweringContext`.", @@ -42025,6 +44035,8 @@ "sig_line": null }, "name": "spl_autoload_unregister", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -42112,6 +44124,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -42120,7 +44133,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.spl_classes` through `BuiltinLoweringContext`.", @@ -42132,6 +44145,8 @@ "sig_line": null }, "name": "spl_classes", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -42219,6 +44234,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -42227,7 +44243,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.spl_object_hash` through `BuiltinLoweringContext`.", @@ -42239,6 +44255,8 @@ "sig_line": null }, "name": "spl_object_hash", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -42334,6 +44352,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -42342,7 +44361,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.spl_object_id` through `BuiltinLoweringContext`.", @@ -42354,6 +44373,8 @@ "sig_line": null }, "name": "spl_object_id", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -42449,6 +44470,7 @@ "variadic": "values" }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -42457,7 +44479,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.sprintf` through `BuiltinLoweringContext`.", @@ -42469,6 +44491,8 @@ "sig_line": null }, "name": "sprintf", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -42563,6 +44587,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -42571,7 +44596,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.sqrt` through `BuiltinLoweringContext`.", @@ -42583,6 +44608,8 @@ "sig_line": null }, "name": "sqrt", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -42666,6 +44693,7 @@ "variadic": "vars" }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -42674,7 +44702,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.sscanf` through `BuiltinLoweringContext`.", @@ -42686,6 +44714,8 @@ "sig_line": null }, "name": "sscanf", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -42788,6 +44818,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -42796,7 +44827,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stat` through `BuiltinLoweringContext`.", @@ -42808,6 +44839,8 @@ "sig_line": null }, "name": "stat", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -42909,6 +44942,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -42917,7 +44951,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.str_contains` through `BuiltinLoweringContext`.", @@ -42929,6 +44963,8 @@ "sig_line": null }, "name": "str_contains", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -43019,6 +45055,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -43027,7 +45064,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.str_ends_with` through `BuiltinLoweringContext`.", @@ -43039,6 +45076,8 @@ "sig_line": null }, "name": "str_ends_with", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -43141,6 +45180,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -43149,7 +45189,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.str_ireplace` through `BuiltinLoweringContext`.", @@ -43161,6 +45201,8 @@ "sig_line": null }, "name": "str_ireplace", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -43277,6 +45319,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -43285,7 +45328,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.str_pad` through `BuiltinLoweringContext`.", @@ -43297,6 +45340,8 @@ "sig_line": null }, "name": "str_pad", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -43401,6 +45446,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -43409,7 +45455,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.str_repeat` through `BuiltinLoweringContext`.", @@ -43421,6 +45467,8 @@ "sig_line": null }, "name": "str_repeat", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -43523,6 +45571,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -43531,7 +45580,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.str_replace` through `BuiltinLoweringContext`.", @@ -43543,6 +45592,8 @@ "sig_line": null }, "name": "str_replace", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -43647,6 +45698,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -43655,7 +45707,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.str_split` through `BuiltinLoweringContext`.", @@ -43667,6 +45719,8 @@ "sig_line": null }, "name": "str_split", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -43758,6 +45812,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -43766,7 +45821,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.str_starts_with` through `BuiltinLoweringContext`.", @@ -43778,6 +45833,8 @@ "sig_line": null }, "name": "str_starts_with", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -43868,6 +45925,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -43876,7 +45934,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.strcasecmp` through `BuiltinLoweringContext`.", @@ -43888,6 +45946,8 @@ "sig_line": null }, "name": "strcasecmp", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -43978,6 +46038,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -43986,7 +46047,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.strcmp` through `BuiltinLoweringContext`.", @@ -43998,6 +46059,8 @@ "sig_line": null }, "name": "strcmp", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -44088,6 +46151,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -44096,7 +46160,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_bucket_append` through `BuiltinLoweringContext`.", @@ -44108,6 +46172,8 @@ "sig_line": null }, "name": "stream_bucket_append", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -44209,6 +46275,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -44217,7 +46284,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_bucket_make_writeable` through `BuiltinLoweringContext`.", @@ -44229,6 +46296,8 @@ "sig_line": null }, "name": "stream_bucket_make_writeable", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -44329,6 +46398,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -44337,7 +46407,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_bucket_new` through `BuiltinLoweringContext`.", @@ -44349,6 +46419,8 @@ "sig_line": null }, "name": "stream_bucket_new", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -44456,6 +46528,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -44464,7 +46537,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_bucket_prepend` through `BuiltinLoweringContext`.", @@ -44476,6 +46549,8 @@ "sig_line": null }, "name": "stream_bucket_prepend", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -44583,6 +46658,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -44591,7 +46667,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_context_create` through `BuiltinLoweringContext`.", @@ -44603,6 +46679,8 @@ "sig_line": null }, "name": "stream_context_create", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -44705,6 +46783,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -44713,7 +46792,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_context_get_default` through `BuiltinLoweringContext`.", @@ -44725,6 +46804,8 @@ "sig_line": null }, "name": "stream_context_get_default", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -44820,6 +46901,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -44828,7 +46910,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_context_get_options` through `BuiltinLoweringContext`.", @@ -44840,6 +46922,8 @@ "sig_line": null }, "name": "stream_context_get_options", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -44935,6 +47019,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -44943,7 +47028,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_context_get_params` through `BuiltinLoweringContext`.", @@ -44955,6 +47040,8 @@ "sig_line": null }, "name": "stream_context_get_params", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -45050,6 +47137,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -45058,7 +47146,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_context_set_default` through `BuiltinLoweringContext`.", @@ -45070,6 +47158,8 @@ "sig_line": null }, "name": "stream_context_set_default", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -45183,6 +47273,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -45191,7 +47282,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_context_set_option` through `BuiltinLoweringContext`.", @@ -45203,6 +47294,8 @@ "sig_line": null }, "name": "stream_context_set_option", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -45324,6 +47417,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -45332,7 +47426,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_context_set_params` through `BuiltinLoweringContext`.", @@ -45344,6 +47438,8 @@ "sig_line": null }, "name": "stream_context_set_params", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -45463,6 +47559,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -45471,7 +47568,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_copy_to_stream` through `BuiltinLoweringContext`.", @@ -45483,6 +47580,8 @@ "sig_line": null }, "name": "stream_copy_to_stream", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -45617,6 +47716,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -45625,7 +47725,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_filter_append` through `BuiltinLoweringContext`.", @@ -45637,6 +47737,8 @@ "sig_line": null }, "name": "stream_filter_append", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -45770,6 +47872,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -45778,7 +47881,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_filter_prepend` through `BuiltinLoweringContext`.", @@ -45790,6 +47893,8 @@ "sig_line": null }, "name": "stream_filter_prepend", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -45911,6 +48016,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -45919,7 +48025,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_filter_register` through `BuiltinLoweringContext`.", @@ -45931,6 +48037,8 @@ "sig_line": null }, "name": "stream_filter_register", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -46033,6 +48141,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -46041,7 +48150,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_filter_remove` through `BuiltinLoweringContext`.", @@ -46053,6 +48162,8 @@ "sig_line": null }, "name": "stream_filter_remove", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -46160,6 +48271,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -46168,7 +48280,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_get_contents` through `BuiltinLoweringContext`.", @@ -46180,6 +48292,8 @@ "sig_line": null }, "name": "stream_get_contents", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -46282,6 +48396,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -46290,7 +48405,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_get_filters` through `BuiltinLoweringContext`.", @@ -46302,6 +48417,8 @@ "sig_line": null }, "name": "stream_get_filters", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -46401,6 +48518,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -46409,7 +48527,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_get_line` through `BuiltinLoweringContext`.", @@ -46421,6 +48539,8 @@ "sig_line": null }, "name": "stream_get_line", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -46530,6 +48650,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -46538,7 +48659,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_get_meta_data` through `BuiltinLoweringContext`.", @@ -46550,6 +48671,8 @@ "sig_line": null }, "name": "stream_get_meta_data", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -46638,6 +48761,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -46646,7 +48770,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_get_transports` through `BuiltinLoweringContext`.", @@ -46658,6 +48782,8 @@ "sig_line": null }, "name": "stream_get_transports", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -46738,6 +48864,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -46746,7 +48873,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_get_wrappers` through `BuiltinLoweringContext`.", @@ -46758,6 +48885,8 @@ "sig_line": null }, "name": "stream_get_wrappers", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -46845,6 +48974,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -46853,7 +48983,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_is_local` through `BuiltinLoweringContext`.", @@ -46865,6 +48995,8 @@ "sig_line": null }, "name": "stream_is_local", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -46959,6 +49091,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -46967,7 +49100,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_isatty` through `BuiltinLoweringContext`.", @@ -46979,6 +49112,8 @@ "sig_line": null }, "name": "stream_isatty", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -47074,6 +49209,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -47082,7 +49218,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_resolve_include_path` through `BuiltinLoweringContext`.", @@ -47094,6 +49230,8 @@ "sig_line": null }, "name": "stream_resolve_include_path", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -47211,6 +49349,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -47219,7 +49358,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_select` through `BuiltinLoweringContext`.", @@ -47231,6 +49370,8 @@ "sig_line": null }, "name": "stream_select", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -47359,6 +49500,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -47367,7 +49509,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_set_blocking` through `BuiltinLoweringContext`.", @@ -47379,6 +49521,8 @@ "sig_line": null }, "name": "stream_set_blocking", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -47487,6 +49631,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -47495,7 +49640,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_set_chunk_size` through `BuiltinLoweringContext`.", @@ -47507,6 +49652,8 @@ "sig_line": null }, "name": "stream_set_chunk_size", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -47614,6 +49761,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -47622,7 +49770,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_set_read_buffer` through `BuiltinLoweringContext`.", @@ -47634,6 +49782,8 @@ "sig_line": null }, "name": "stream_set_read_buffer", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -47747,6 +49897,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -47755,7 +49906,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_set_timeout` through `BuiltinLoweringContext`.", @@ -47767,6 +49918,8 @@ "sig_line": null }, "name": "stream_set_timeout", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -47882,6 +50035,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -47890,7 +50044,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_set_write_buffer` through `BuiltinLoweringContext`.", @@ -47902,6 +50056,8 @@ "sig_line": null }, "name": "stream_set_write_buffer", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -48014,6 +50170,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -48022,7 +50179,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_socket_accept` through `BuiltinLoweringContext`.", @@ -48034,6 +50191,8 @@ "sig_line": null }, "name": "stream_socket_accept", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -48143,6 +50302,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -48151,7 +50311,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_socket_client` through `BuiltinLoweringContext`.", @@ -48163,6 +50323,8 @@ "sig_line": null }, "name": "stream_socket_client", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -48199,8 +50361,7 @@ "kind": "may_alias_arguments" }, "requirements": { - "kind": "static", - "values": [] + "kind": "shared" }, "result_type": "checked", "runtime_functions": [ @@ -48276,6 +50437,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -48284,7 +50446,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_socket_enable_crypto` through `BuiltinLoweringContext`.", @@ -48296,6 +50458,8 @@ "sig_line": null }, "name": "stream_socket_enable_crypto", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -48386,7 +50550,7 @@ "type": "resource" } ], - "return_type": "bool", + "return_type": "mixed", "variadic": null }, "slug": "stream_socket_enable_crypto", @@ -48423,6 +50587,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -48431,7 +50596,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_socket_get_name` through `BuiltinLoweringContext`.", @@ -48443,6 +50608,8 @@ "sig_line": null }, "name": "stream_socket_get_name", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -48557,6 +50724,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -48565,7 +50733,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_socket_pair` through `BuiltinLoweringContext`.", @@ -48577,6 +50745,8 @@ "sig_line": null }, "name": "stream_socket_pair", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -48702,6 +50872,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -48710,7 +50881,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_socket_recvfrom` through `BuiltinLoweringContext`.", @@ -48722,6 +50893,8 @@ "sig_line": null }, "name": "stream_socket_recvfrom", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -48856,6 +51029,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -48864,7 +51038,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_socket_sendto` through `BuiltinLoweringContext`.", @@ -48876,6 +51050,8 @@ "sig_line": null }, "name": "stream_socket_sendto", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -48992,6 +51168,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -49000,7 +51177,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_socket_server` through `BuiltinLoweringContext`.", @@ -49012,6 +51189,8 @@ "sig_line": null }, "name": "stream_socket_server", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -49113,6 +51292,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -49121,7 +51301,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_socket_shutdown` through `BuiltinLoweringContext`.", @@ -49133,6 +51313,8 @@ "sig_line": null }, "name": "stream_socket_shutdown", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -49235,6 +51417,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -49243,7 +51426,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_supports_lock` through `BuiltinLoweringContext`.", @@ -49255,6 +51438,8 @@ "sig_line": null }, "name": "stream_supports_lock", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -49362,6 +51547,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -49370,7 +51556,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_wrapper_register` through `BuiltinLoweringContext`.", @@ -49382,6 +51568,8 @@ "sig_line": null }, "name": "stream_wrapper_register", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -49491,6 +51679,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -49499,7 +51688,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_wrapper_restore` through `BuiltinLoweringContext`.", @@ -49511,6 +51700,8 @@ "sig_line": null }, "name": "stream_wrapper_restore", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -49605,6 +51796,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -49613,7 +51805,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_wrapper_unregister` through `BuiltinLoweringContext`.", @@ -49625,6 +51817,8 @@ "sig_line": null }, "name": "stream_wrapper_unregister", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -49719,6 +51913,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -49727,7 +51922,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.string.strip_slashes` through `BuiltinLoweringContext`.", @@ -49739,6 +51934,8 @@ "sig_line": null }, "name": "stripslashes", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -49813,6 +52010,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -49821,7 +52019,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `eir_graph` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -49832,6 +52030,8 @@ "sig_line": null }, "name": "strlen", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -49916,6 +52116,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -49924,7 +52125,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.strpos` through `BuiltinLoweringContext`.", @@ -49936,6 +52137,8 @@ "sig_line": null }, "name": "strpos", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -50028,6 +52231,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -50036,7 +52240,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.string.reverse` through `BuiltinLoweringContext`.", @@ -50048,6 +52252,8 @@ "sig_line": null }, "name": "strrev", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -50134,6 +52340,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -50142,7 +52349,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.strrpos` through `BuiltinLoweringContext`.", @@ -50154,6 +52361,8 @@ "sig_line": null }, "name": "strrpos", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -50258,6 +52467,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -50266,7 +52476,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.strstr` through `BuiltinLoweringContext`.", @@ -50278,6 +52488,8 @@ "sig_line": null }, "name": "strstr", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -50369,6 +52581,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -50377,7 +52590,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.string.to_lower` through `BuiltinLoweringContext`.", @@ -50389,6 +52602,8 @@ "sig_line": null }, "name": "strtolower", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -50469,6 +52684,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -50477,7 +52693,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.strtotime` through `BuiltinLoweringContext`.", @@ -50489,6 +52705,8 @@ "sig_line": null }, "name": "strtotime", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -50525,8 +52743,7 @@ "kind": "may_alias_arguments" }, "requirements": { - "kind": "static", - "values": [] + "kind": "shared" }, "result_type": "checked", "runtime_functions": [ @@ -50591,6 +52808,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -50599,7 +52817,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.string.to_upper` through `BuiltinLoweringContext`.", @@ -50611,6 +52829,8 @@ "sig_line": null }, "name": "strtoupper", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -50685,6 +52905,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -50693,7 +52914,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -50704,6 +52925,8 @@ "sig_line": null }, "name": "strval", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -50789,6 +53012,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -50797,7 +53021,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.substr` through `BuiltinLoweringContext`.", @@ -50809,6 +53033,8 @@ "sig_line": null }, "name": "substr", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -50918,6 +53144,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -50926,7 +53153,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.substr_replace` through `BuiltinLoweringContext`.", @@ -50938,6 +53165,8 @@ "sig_line": null }, "name": "substr_replace", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -51042,6 +53271,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -51050,7 +53280,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.symlink` through `BuiltinLoweringContext`.", @@ -51062,6 +53292,8 @@ "sig_line": null }, "name": "symlink", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -51156,6 +53388,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -51164,7 +53397,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.sys_get_temp_dir` through `BuiltinLoweringContext`.", @@ -51176,6 +53409,8 @@ "sig_line": null }, "name": "sys_get_temp_dir", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -51262,6 +53497,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -51270,7 +53506,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.system` through `BuiltinLoweringContext`.", @@ -51282,6 +53518,8 @@ "sig_line": null }, "name": "system", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -51376,6 +53614,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -51384,7 +53623,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.tan` through `BuiltinLoweringContext`.", @@ -51396,6 +53635,8 @@ "sig_line": null }, "name": "tan", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -51473,6 +53714,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -51481,7 +53723,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.tanh` through `BuiltinLoweringContext`.", @@ -51493,6 +53735,8 @@ "sig_line": null }, "name": "tanh", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -51576,6 +53820,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -51584,7 +53829,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.tempnam` through `BuiltinLoweringContext`.", @@ -51596,6 +53841,8 @@ "sig_line": null }, "name": "tempnam", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -51635,7 +53882,7 @@ "kind": "static", "values": [] }, - "result_type": "declared", + "result_type": "checked", "runtime_functions": [ "tempnam" ], @@ -51646,7 +53893,8 @@ "linux-x86_64" ], "validation": { - "kind": "signature" + "kind": "checker_hook", + "lazy": false } }, "sig": { @@ -51666,7 +53914,7 @@ "type": "string" } ], - "return_type": "string", + "return_type": "mixed", "variadic": null }, "slug": "tempnam", @@ -51690,6 +53938,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -51698,7 +53947,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.time` through `BuiltinLoweringContext`.", @@ -51710,6 +53959,8 @@ "sig_line": null }, "name": "time", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -51789,6 +54040,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -51797,7 +54049,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.tmpfile` through `BuiltinLoweringContext`.", @@ -51809,6 +54061,8 @@ "sig_line": null }, "name": "tmpfile", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -51908,6 +54162,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -51916,7 +54171,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.touch` through `BuiltinLoweringContext`.", @@ -51928,6 +54183,8 @@ "sig_line": null }, "name": "touch", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -52043,6 +54300,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -52051,7 +54309,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.trait_exists` through `BuiltinLoweringContext`.", @@ -52063,6 +54321,8 @@ "sig_line": null }, "name": "trait_exists", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -52171,6 +54431,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -52179,7 +54440,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.trim` through `BuiltinLoweringContext`.", @@ -52191,6 +54452,8 @@ "sig_line": null }, "name": "trim", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -52280,6 +54543,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -52288,7 +54552,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.uasort` through `BuiltinLoweringContext`.", @@ -52300,6 +54564,8 @@ "sig_line": null }, "name": "uasort", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "user_value_sort", "callable": { @@ -52402,6 +54668,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -52410,7 +54677,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ucfirst` through `BuiltinLoweringContext`.", @@ -52422,6 +54689,8 @@ "sig_line": null }, "name": "ucfirst", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -52505,6 +54774,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -52513,7 +54783,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ucwords` through `BuiltinLoweringContext`.", @@ -52525,6 +54795,8 @@ "sig_line": null }, "name": "ucwords", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -52614,6 +54886,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -52622,7 +54895,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.uksort` through `BuiltinLoweringContext`.", @@ -52634,6 +54907,8 @@ "sig_line": null }, "name": "uksort", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -52736,6 +55011,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -52744,7 +55020,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.umask` through `BuiltinLoweringContext`.", @@ -52756,6 +55032,8 @@ "sig_line": null }, "name": "umask", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -52850,6 +55128,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -52858,7 +55137,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.unlink` through `BuiltinLoweringContext`.", @@ -52870,6 +55149,8 @@ "sig_line": null }, "name": "unlink", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -52948,6 +55229,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -52956,7 +55238,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.unserialize` through `BuiltinLoweringContext`.", @@ -52968,6 +55250,8 @@ "sig_line": null }, "name": "unserialize", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -53070,6 +55354,7 @@ "variadic": "vars" }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -53088,6 +55373,8 @@ "sig_line": null }, "name": "unset", + "notes": [], + "see_also": [], "semantics": null, "sig": { "params": [ @@ -53130,6 +55417,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -53138,7 +55426,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.string.url_decode` through `BuiltinLoweringContext`.", @@ -53150,6 +55438,8 @@ "sig_line": null }, "name": "urldecode", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -53224,6 +55514,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -53232,7 +55523,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.string.url_encode` through `BuiltinLoweringContext`.", @@ -53244,6 +55535,8 @@ "sig_line": null }, "name": "urlencode", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -53318,6 +55611,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -53326,7 +55620,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.usleep` through `BuiltinLoweringContext`.", @@ -53338,6 +55632,8 @@ "sig_line": null }, "name": "usleep", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -53437,6 +55733,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -53445,7 +55742,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.usort` through `BuiltinLoweringContext`.", @@ -53457,6 +55754,8 @@ "sig_line": null }, "name": "usort", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "user_value_sort", "callable": { @@ -53559,6 +55858,7 @@ "variadic": "values" }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -53567,7 +55867,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.var_dump` through `BuiltinLoweringContext`.", @@ -53579,6 +55879,8 @@ "sig_line": null }, "name": "var_dump", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -53685,6 +55987,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -53693,7 +55996,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.vfprintf` through `BuiltinLoweringContext`.", @@ -53705,6 +56008,8 @@ "sig_line": null }, "name": "vfprintf", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -53820,6 +56125,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -53828,7 +56134,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.vprintf` through `BuiltinLoweringContext`.", @@ -53840,6 +56146,8 @@ "sig_line": null }, "name": "vprintf", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -53947,6 +56255,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -53955,7 +56264,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.vsprintf` through `BuiltinLoweringContext`.", @@ -53967,6 +56276,8 @@ "sig_line": null }, "name": "vsprintf", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -54086,6 +56397,7 @@ "variadic": null }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": false, "is_internal": false, @@ -54094,7 +56406,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.wordwrap` through `BuiltinLoweringContext`.", @@ -54106,6 +56418,8 @@ "sig_line": null }, "name": "wordwrap", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -54188,6 +56502,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": true, "is_internal": false, @@ -54196,7 +56511,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.zval_free` through `BuiltinLoweringContext`.", @@ -54208,6 +56523,8 @@ "sig_line": null }, "name": "zval_free", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -54287,6 +56604,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": true, "is_internal": false, @@ -54295,7 +56613,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.zval_pack` through `BuiltinLoweringContext`.", @@ -54307,6 +56625,8 @@ "sig_line": null }, "name": "zval_pack", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -54386,6 +56706,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": true, "is_internal": false, @@ -54394,7 +56715,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.zval_type` through `BuiltinLoweringContext`.", @@ -54406,6 +56727,8 @@ "sig_line": null }, "name": "zval_type", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { @@ -54485,6 +56808,7 @@ "supported": false }, "eval_only": false, + "examples": [], "in_catalog": true, "is_extension": true, "is_internal": false, @@ -54493,7 +56817,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 423, + "codegen_line": 426, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.zval_unpack` through `BuiltinLoweringContext`.", @@ -54505,6 +56829,8 @@ "sig_line": null }, "name": "zval_unpack", + "notes": [], + "see_also": [], "semantics": { "argument_lowering": "standard", "callable": { diff --git a/scripts/docs/elephc_builtins/extract.py b/scripts/docs/elephc_builtins/extract.py index 0a3b5c28f9..f5d4bc9476 100644 --- a/scripts/docs/elephc_builtins/extract.py +++ b/scripts/docs/elephc_builtins/extract.py @@ -45,6 +45,7 @@ Builtin, BuiltinSig, DESCRIPTION_OVERRIDES, + DOCUMENTATION_OVERRIDES, INTERNAL_NOTES, LoweringInfo, PARAM_TYPES, @@ -612,6 +613,7 @@ def resolve_check_body(fn_name: str) -> str: lowering.notes = INTERNAL_NOTES[canonical] + lowering.notes area = resolve_registry_area(canonical, entry["area"]) + documentation = DOCUMENTATION_OVERRIDES.get(canonical, {}) builtins.append( Builtin( @@ -628,6 +630,9 @@ def resolve_check_body(fn_name: str) -> str: ), lowering=lowering, description=description, + examples=list(documentation.get("examples", [])), + see_also=list(documentation.get("see_also", [])), + notes=list(documentation.get("notes", [])), eval_support=entry.get("eval"), is_extension=bool(entry.get("extension")), semantics=entry.get("semantics"), @@ -709,6 +714,9 @@ def _builtin_to_dict(b: Builtin) -> dict: "is_internal": b.is_internal, "is_extension": b.is_extension, "description": b.description, + "examples": b.examples, + "see_also": b.see_also, + "notes": b.notes, "sig": { "params": [ { diff --git a/scripts/docs/elephc_builtins/registry.py b/scripts/docs/elephc_builtins/registry.py index 04ae198661..57b490336b 100644 --- a/scripts/docs/elephc_builtins/registry.py +++ b/scripts/docs/elephc_builtins/registry.py @@ -483,6 +483,7 @@ "rand": ["int", "int"], "mt_rand": ["int", "int"], "random_int": ["int", "int"], + "random_bytes": ["int"], "is_nan": ["float"], "is_finite": ["float"], "is_infinite": ["float"], @@ -614,7 +615,14 @@ "shell_exec": ["string"], "popen": ["string", "string"], "pclose": ["resource"], - "proc_open": ["string", "array", "array", "string", "array", "array"], + "proc_open": [ + "array|string", + "array", + "array", + "?string", + "?array", + "?array", + ], "proc_close": ["resource"], "proc_get_status": ["resource"], "proc_terminate": ["resource", "int"], @@ -850,7 +858,14 @@ "date_parse_from_format": ["string", "string"], "date_sun_info": ["int", "int"], # --- Process (round 2) --- - "proc_open": ["string", "array", "array", "string", "array", "array"], + "proc_open": [ + "array|string", + "array", + "array", + "?string", + "?array", + "?array", + ], "proc_close": ["resource"], "proc_get_status": ["resource"], "proc_terminate": ["resource", "int"], @@ -1256,6 +1271,8 @@ def slug(name: str) -> str: "vprintf": "int", "vsprintf": "string", "iterator_apply": "int", + "proc_get_status": "array|false", + "proc_open": "resource|false", "zval_pack": "pointer", "zval_unpack": "mixed", "zval_type": "int", @@ -1286,6 +1303,67 @@ def slug(name: str) -> str: } +# Structured user-facing details that cannot be recovered from the scalar +# builtin descriptor alone. Keep target-specific behavior here so forced docs +# regeneration preserves it instead of replacing it with a generic stub. +DOCUMENTATION_OVERRIDES: Dict[str, Dict[str, List[str]]] = { + "escapeshellarg": { + "examples": [ + "See [`examples/shell-escaping`](../../../../examples/shell-escaping/main.php) " + "for argument and command escaping together." + ], + "notes": [ + "macOS and Linux use PHP's single-quoted shell form; Windows uses PHP's " + "double-quoted command-line escaping rules.", + "Embedded NUL bytes raise a catchable `ValueError`.", + "Windows enforces PHP's 8192-byte command-line limit for both input and output.", + ], + }, + "escapeshellcmd": { + "examples": [ + "See [`examples/shell-escaping`](../../../../examples/shell-escaping/main.php) " + "for argument and command escaping together." + ], + "notes": [ + "Shell metacharacters follow PHP's platform-specific POSIX or Windows rules.", + "Embedded NUL bytes raise a catchable `ValueError`.", + "Windows enforces PHP's 8192-byte command-line limit for both input and output.", + ], + }, + "proc_get_status": { + "notes": [ + "The status record contains `command`, `pid`, `cached`, `running`, " + "`signaled`, `stopped`, `exitcode`, `termsig`, and `stopsig`.", + "Windows status queries do not reap the process and report `cached` as `false`.", + "Unix caches a normally exited child so `proc_close()` can still return its exit code.", + ], + }, + "proc_open": { + "examples": [ + "See [`examples/process-pipes`](../../../../examples/process-pipes/main.php) " + "for a complete stdin/stdout/stderr exchange and process-status query." + ], + "notes": [ + "Windows accepts string and array commands, UTF-8 working directories and " + "environment maps, plus PHP's documented process options.", + "The Windows descriptor runtime supports `pipe`, `socket`, `file`, `redirect`, " + "stream-resource, and `null` entries while preserving sparse integer keys.", + "Descriptors above 2 are rejected on Windows because `STARTUPINFOW` cannot expose " + "them as matching numbered CRT descriptors.", + "The Windows `blocking_pipes` option selects blocking `ReadFile` behavior; the " + "default probes readable pipes and reports `EAGAIN` when no bytes are available.", + ], + }, + "proc_terminate": { + "notes": [ + "Unix forwards the optional signal to `kill(2)`.", + "Windows follows PHP by ignoring the signal value and terminating the process " + "with exit code 255.", + ], + }, +} + + # Notes for compiler-internal helpers. These appear on the internals pages for # the __elephc_* builtins that have no user-facing reference. INTERNAL_NOTES: Dict[str, List[str]] = { diff --git a/scripts/docs/elephc_builtins/render.py b/scripts/docs/elephc_builtins/render.py index 0797ed201e..55f3224529 100644 --- a/scripts/docs/elephc_builtins/render.py +++ b/scripts/docs/elephc_builtins/render.py @@ -123,17 +123,13 @@ def _internals_short_description(b: dict) -> str: def _signature_line(b: dict) -> str: parts: list[str] = [] for p in b["sig"]["params"]: - prefix = "" - if p["by_ref"]: - prefix += "&" - if not p.get("optional"): - prefix += "" + reference = "&" if p["by_ref"] else "" # PHP-style render: `string $name` if p.get("default") is not None: # already rendered PHP literal - parts.append(f"{p['type']} ${p['name']} = {p['default']}") + parts.append(f"{p['type']} {reference}${p['name']} = {p['default']}") else: - parts.append(f"{p['type']} ${p['name']}") + parts.append(f"{p['type']} {reference}${p['name']}") params = ", ".join(parts) if b["sig"]["variadic"]: sep = ", " if parts else "" diff --git a/scripts/gen_windows_codegen_allowlist.py b/scripts/gen_windows_codegen_allowlist.py new file mode 100755 index 0000000000..e4a179a93d --- /dev/null +++ b/scripts/gen_windows_codegen_allowlist.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +"""Verify exact successful coverage of the native Windows codegen shards. + +The aggregate CI job supplies the runnable ``ci``-profile inventory and every +native Windows shard's JUnit report. This tool rejects missing, duplicate, +unexpected, or failing testcases so neither a failed test nor a truncated +partition can masquerade as a green Windows result. +""" + +from __future__ import annotations + +import argparse +import json +import sys +import xml.etree.ElementTree as ET +from collections import Counter +from pathlib import Path + +# The nextest test binary whose fixtures make up the strict native 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 parse_junit_cases(path: Path) -> list[str]: + """Return every named testcase in report order, retaining duplicates.""" + root = ET.parse(path).getroot() + return [ + name + for testcase in root.iter("testcase") + if (name := testcase.attrib.get("name")) is not 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 cmd_verify_complete(args: argparse.Namespace) -> int: + """Reject incomplete, duplicate, stale, or optionally failing shard coverage.""" + if ( + args.expected_junit_count is not None + and len(args.junit) != args.expected_junit_count + ): + print( + "WINDOWS CODEGEN INCOMPLETE: expected " + f"{args.expected_junit_count} JUnit report(s), found {len(args.junit)}.", + file=sys.stderr, + ) + return 1 + + runnable = load_runnable(Path(args.list_json)) + observed_list = [ + name + for junit_path in args.junit + for name in parse_junit_cases(Path(junit_path)) + ] + failures = sorted( + { + name + for junit_path in args.junit + for name in parse_junit_failures(Path(junit_path))[0] + } + ) + observed = set(observed_list) + missing = sorted(runnable - observed) + unexpected = sorted(observed - runnable) + duplicates = sorted( + name for name, count in Counter(observed_list).items() if count > 1 + ) + + if missing or unexpected or duplicates or (args.require_success and failures): + print( + "WINDOWS CODEGEN INCOMPLETE: aggregate shard coverage does not " + "provide one successful execution of every runnable CI test.", + file=sys.stderr, + ) + for label, names in ( + ("missing", missing), + ("unexpected", unexpected), + ("duplicated", duplicates), + ("failed", failures if args.require_success else []), + ): + if not names: + continue + print(f"{label} testcase(s): {len(names)}", file=sys.stderr) + for name in names[:50]: + print(f" {name}", file=sys.stderr) + if len(names) > 50: + print(f" ... and {len(names) - 50} more", file=sys.stderr) + return 1 + + print( + f"OK: {len(observed)} Windows codegen testcase(s) exactly cover " + "the runnable CI set once each." + ) + return 0 + + +def build_parser() -> argparse.ArgumentParser: + """Construct the CLI for strict native Windows coverage verification.""" + p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + sub = p.add_subparsers(dest="command", required=True) + + c = sub.add_parser( + "verify-complete", + help="verify aggregate shard coverage against the runnable test set", + ) + c.add_argument("--list-json", required=True, help="nextest runnable-list JSON") + c.add_argument( + "--junit", + action="append", + required=True, + help="per-shard nextest JUnit report", + ) + c.add_argument( + "--expected-junit-count", + type=int, + help="reject the aggregate unless exactly this many JUnit reports are supplied", + ) + c.add_argument( + "--require-success", + action="store_true", + help="also reject every failure/error recorded in the shard reports", + ) + c.set_defaults(func=cmd_verify_complete) + 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/scripts/tests/test_builtin_docs.py b/scripts/tests/test_builtin_docs.py new file mode 100644 index 0000000000..bafefdda52 --- /dev/null +++ b/scripts/tests/test_builtin_docs.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Regression tests for generated builtin signatures and documentation metadata.""" + +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + + +DOCS_PACKAGE = Path(__file__).resolve().parents[1] / "docs" / "elephc_builtins" +sys.path.insert(0, str(DOCS_PACKAGE)) + +import extract # noqa: E402 +import registry # noqa: E402 +import render # noqa: E402 + + +class BuiltinDocsTests(unittest.TestCase): + """Protect PHP-visible contracts that the scalar registry cannot express alone.""" + + def test_signature_renders_by_reference_parameters(self) -> None: + """Renders the PHP reference marker between the type and variable sigil.""" + builtin = { + "name": "proc_open", + "sig": { + "params": [ + { + "name": "pipes", + "type": "array", + "by_ref": True, + "default": None, + "optional": False, + } + ], + "variadic": None, + "return_type": "resource|false", + }, + } + + self.assertEqual( + render._signature_line(builtin), + "function proc_open(array &$pipes): resource|false", + ) + + def test_proc_open_overrides_preserve_php_types(self) -> None: + """Keeps the command union, nullable options, and resource failure union.""" + self.assertEqual( + registry.PARAM_TYPES["proc_open"], + ["array|string", "array", "array", "?string", "?array", "?array"], + ) + self.assertEqual( + registry.RETURN_TYPE_OVERRIDES["proc_open"], + "resource|false", + ) + self.assertEqual( + registry.RETURN_TYPE_OVERRIDES["proc_get_status"], + "array|false", + ) + + def test_documentation_metadata_is_serialized(self) -> None: + """Carries structured notes and examples into the renderer's JSON schema.""" + builtin = registry.Builtin( + name="proc_open", + canonical_name="proc_open", + area="Process", + sub_area="Process", + in_catalog=True, + is_internal=False, + examples=["example"], + see_also=["proc_close"], + notes=["Windows behavior"], + ) + + serialized = extract._builtin_to_dict(builtin) + + self.assertEqual(serialized["examples"], ["example"]) + self.assertEqual(serialized["see_also"], ["proc_close"]) + self.assertEqual(serialized["notes"], ["Windows behavior"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/tests/test_check_bridge_exports.py b/scripts/tests/test_check_bridge_exports.py new file mode 100755 index 0000000000..a2917b1b72 --- /dev/null +++ b/scripts/tests/test_check_bridge_exports.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +"""Structural tests for the bridge static-archive export verifier.""" + +from __future__ import annotations + +import importlib.util +import tempfile +import unittest +from pathlib import Path + + +SCRIPT = Path(__file__).resolve().parents[1] / "check_bridge_exports.py" +SPEC = importlib.util.spec_from_file_location("check_bridge_exports", SCRIPT) +assert SPEC and SPEC.loader +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +class BridgeExportTests(unittest.TestCase): + """Exercise source and nm parsing without requiring a cross-toolchain.""" + + def test_source_exports_accepts_safe_and_unsafe_functions(self) -> None: + """The source scanner recognizes both safe and unsafe C ABI exports.""" + with tempfile.TemporaryDirectory() as directory: + source = Path(directory) / "lib.rs" + source.write_text( + '#[no_mangle]\npub extern "C" fn elephc_safe() {}\n' + '#[no_mangle]\npub unsafe extern "C" fn elephc_unsafe() {}\n' + 'pub extern "C" fn elephc_mangled() {}\n', + encoding="utf-8", + ) + self.assertEqual( + MODULE.source_exports(Path(directory)), + {"elephc_safe", "elephc_unsafe"}, + ) + + def test_nm_symbol_regex_accepts_coff_decoration(self) -> None: + """The nm parser accepts undecorated x64 and leading-underscore symbols.""" + self.assertEqual( + MODULE.NM_SYMBOL_RE.match("elephc_tls_read").group(1), + "elephc_tls_read", + ) + self.assertEqual( + MODULE.NM_SYMBOL_RE.match("_elephc_tls_read").group(1), + "elephc_tls_read", + ) + self.assertIsNone(MODULE.NM_SYMBOL_RE.match("rust_eh_personality")) + + def test_archive_path_accepts_msvc_staticlibs(self) -> None: + """Cargo's native MSVC .lib naming is recognized alongside GNU .a.""" + with tempfile.TemporaryDirectory() as directory: + staticlib = Path(directory) / "debug" / "elephc_tls.lib" + staticlib.parent.mkdir() + staticlib.touch() + self.assertEqual( + MODULE.archive_path(Path(directory), "", "debug", "tls"), + staticlib, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/tests/test_gen_windows_codegen_allowlist.py b/scripts/tests/test_gen_windows_codegen_allowlist.py new file mode 100644 index 0000000000..c1d2eb6407 --- /dev/null +++ b/scripts/tests/test_gen_windows_codegen_allowlist.py @@ -0,0 +1,147 @@ +"""Regression tests for strict native Windows codegen coverage verification.""" + +from __future__ import annotations + +import json +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] +SCRIPT = REPO_ROOT / "scripts" / "gen_windows_codegen_allowlist.py" + + +class WindowsCodegenCoverageTests(unittest.TestCase): + """Exercises strict native shard coverage through the public CLI.""" + + def test_baseline_commands_are_not_exposed(self) -> None: + """Keeps allow-list generation and failure exemptions out of the strict gate.""" + for removed_command in ("generate", "gate"): + result = subprocess.run( + [sys.executable, str(SCRIPT), removed_command], + check=False, + capture_output=True, + text=True, + ) + self.assertNotEqual(result.returncode, 0) + self.assertIn("invalid choice", result.stderr) + + def run_complete( + self, + runnable: list[str], + shard_cases: list[list[str]], + failed: set[str] | None = None, + require_success: bool = False, + expected_junit_count: int | None = None, + ) -> subprocess.CompletedProcess[str]: + """Runs aggregate coverage verification for synthetic shard reports.""" + failed = failed or set() + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + list_path = root / "list.json" + testcases = { + name: {"ignored": False, "filter-match": {"status": "matches"}} + for name in runnable + } + list_path.write_text( + json.dumps( + { + "rust-suites": { + "elephc::codegen_tests": {"testcases": testcases} + } + } + ), + encoding="utf-8", + ) + reports: list[Path] = [] + for index, names in enumerate(shard_cases): + report = root / f"shard-{index}.xml" + cases = "".join( + ( + f'' + if name in failed + else f'' + ) + for name in names + ) + report.write_text( + f'{cases}', + encoding="utf-8", + ) + reports.append(report) + + command = [ + sys.executable, + str(SCRIPT), + "verify-complete", + "--list-json", + str(list_path), + ] + for report in reports: + command.extend(["--junit", str(report)]) + if require_success: + command.append("--require-success") + if expected_junit_count is not None: + command.extend( + ["--expected-junit-count", str(expected_junit_count)] + ) + return subprocess.run( + command, check=False, capture_output=True, text=True + ) + + def test_complete_shards_cover_runnable_set_once(self) -> None: + """Accepts an exact one-time partition of the runnable fixtures.""" + result = self.run_complete( + ["codegen::a", "codegen::b"], + [["codegen::a"], ["codegen::b"]], + expected_junit_count=2, + ) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("exactly cover", result.stdout) + + def test_wrong_junit_report_count_is_rejected(self) -> None: + """Rejects an aggregate whose report count differs from the CI shard count.""" + result = self.run_complete( + ["codegen::a"], + [["codegen::a"]], + expected_junit_count=16, + ) + + self.assertEqual(result.returncode, 1) + self.assertIn("expected 16 JUnit report(s), found 1", result.stderr) + + def test_incomplete_shards_are_rejected(self) -> None: + """Rejects a truncated aggregate report even when all observed tests pass.""" + result = self.run_complete(["codegen::a", "codegen::b"], [["codegen::a"]]) + + self.assertEqual(result.returncode, 1) + self.assertIn("missing testcase(s): 1", result.stderr) + self.assertIn("codegen::b", result.stderr) + + def test_duplicate_shard_case_is_rejected(self) -> None: + """Rejects overlapping partitions that execute one fixture twice.""" + result = self.run_complete(["codegen::a"], [["codegen::a"], ["codegen::a"]]) + + self.assertEqual(result.returncode, 1) + self.assertIn("duplicated testcase(s): 1", result.stderr) + + def test_strict_complete_verification_rejects_failures(self) -> None: + """Rejects a complete native Windows run when any fixture failed.""" + result = self.run_complete( + ["codegen::a", "codegen::b"], + [["codegen::a"], ["codegen::b"]], + failed={"codegen::b"}, + require_success=True, + ) + + self.assertEqual(result.returncode, 1) + self.assertIn("failed testcase(s): 1", result.stderr) + self.assertIn("codegen::b", result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/autoload/mod.rs b/src/autoload/mod.rs index d4411c82a3..52594a0df5 100644 --- a/src/autoload/mod.rs +++ b/src/autoload/mod.rs @@ -21,6 +21,7 @@ use std::path::{Path, PathBuf}; pub use registry::Registry; +use crate::codegen::platform::Platform; use crate::errors::CompileError; use crate::parser::ast::Program; use crate::parser::ast::Stmt; @@ -110,10 +111,21 @@ const BUILTIN_CLASS_LIKE_NAMES: &[&str] = &[ /// the program, look it up first in the composer.json PSR-4 index and /// then in the user-registered closure rules; parse the referenced file, /// run resolver+name_resolver on it, and append. Iterate until stable. +#[allow(dead_code)] pub fn run( + program: Program, + base_dir: &Path, + registry: &Registry, +) -> Result { + run_for_platform(program, base_dir, registry, Platform::detect_host()) +} + +/// Runs autoload resolution against the selected target platform's builtin surface. +pub fn run_for_platform( mut program: Program, base_dir: &Path, registry: &Registry, + platform: Platform, ) -> Result { if registry.is_empty() { return Ok(program); @@ -129,7 +141,7 @@ pub fn run( for path in registry.always_included_files() { let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); if included.insert(canonical.clone()) { - prefix.extend(load_autoloaded_file(&canonical, base_dir)?); + prefix.extend(load_autoloaded_file(&canonical, base_dir, platform)?); } } if !prefix.is_empty() { @@ -149,7 +161,7 @@ pub fn run( if let Some(path) = resolve_class(&fqn, registry) { let canonical = path.canonicalize().unwrap_or(path); if included.insert(canonical.clone()) { - let loaded = load_autoloaded_file(&canonical, base_dir)?; + let loaded = load_autoloaded_file(&canonical, base_dir, platform)?; insertions.push((stmt_idx, loaded)); } } @@ -200,7 +212,11 @@ fn resolve_class(fqn: &str, registry: &Registry) -> Option { } /// Load, parse, and resolve a single autoloaded PHP file, returning its statements. -fn load_autoloaded_file(path: &Path, base_dir: &Path) -> Result { +fn load_autoloaded_file( + path: &Path, + base_dir: &Path, + platform: Platform, +) -> Result { let content = std::fs::read_to_string(path).map_err(|e| { CompileError::new( Span::dummy(), @@ -216,7 +232,8 @@ fn load_autoloaded_file(path: &Path, base_dir: &Path) -> Result = crate::name_resolver::resolve(resolved)?; + let canonicalized: Vec = + crate::name_resolver::resolve_for_platform(resolved, platform)?; // name_resolver has already flattened namespace nodes and canonicalized // declarations, so we splice the statements directly into the top-level // program. diff --git a/src/builtins/docs.rs b/src/builtins/docs.rs index f2b39bc3e8..957e8f5770 100644 --- a/src/builtins/docs.rs +++ b/src/builtins/docs.rs @@ -74,6 +74,9 @@ fn requirement_json(requirement: BuiltinRequirement) -> Value { BuiltinRequirement::MacOsLibrary(name) => { json!({"kind": "macos_library", "name": name}) } + BuiltinRequirement::WindowsLibrary(name) => { + json!({"kind": "windows_library", "name": name}) + } BuiltinRequirement::RuntimeFeature(name) => { json!({"kind": "runtime_feature", "name": name}) } @@ -142,6 +145,7 @@ fn semantics_json(semantics: BuiltinSemantics) -> Value { BuiltinArgumentLowering::Count => "count", BuiltinArgumentLowering::Date => "date", BuiltinArgumentLowering::JsonDecode => "json_decode", + BuiltinArgumentLowering::ProcOpen => "proc_open", BuiltinArgumentLowering::PregReplaceCallback => "preg_replace_callback", BuiltinArgumentLowering::PositionalRegex => "positional_regex", BuiltinArgumentLowering::UserValueSort => "user_value_sort", diff --git a/src/builtins/io/__elephc_phar_sign_hash.rs b/src/builtins/io/__elephc_phar_sign_hash.rs index 3d6bda4bb5..59154cd83b 100644 --- a/src/builtins/io/__elephc_phar_sign_hash.rs +++ b/src/builtins/io/__elephc_phar_sign_hash.rs @@ -15,7 +15,7 @@ builtin! { name: "__elephc_phar_sign_hash", area: Io, - params: [path: Str, algo: Str], + params: [path: Str, algo: Int], returns: Bool, semantics: crate::builtins::semantics::runtime_fn_semantics( crate::ir::RuntimeFnId::ElephcPharSignHash, diff --git a/src/builtins/io/chgrp.rs b/src/builtins/io/chgrp.rs index d8a49d4ac4..dc6d8c0d29 100644 --- a/src/builtins/io/chgrp.rs +++ b/src/builtins/io/chgrp.rs @@ -15,7 +15,7 @@ use crate::types::PhpType; builtin! { name: "chgrp", area: Io, - params: [filename: Str, group: Str], + params: [filename: Str, group: Mixed], returns: Bool, check: check, semantics: crate::builtins::semantics::runtime_fn_semantics( diff --git a/src/builtins/io/chown.rs b/src/builtins/io/chown.rs index 76852b64b3..99567a491a 100644 --- a/src/builtins/io/chown.rs +++ b/src/builtins/io/chown.rs @@ -15,7 +15,7 @@ use crate::types::PhpType; builtin! { name: "chown", area: Io, - params: [filename: Str, user: Str], + params: [filename: Str, user: Mixed], returns: Bool, check: check, semantics: crate::builtins::semantics::runtime_fn_semantics( diff --git a/src/builtins/io/filemtime.rs b/src/builtins/io/filemtime.rs index 55fd972fab..4d619eb9e8 100644 --- a/src/builtins/io/filemtime.rs +++ b/src/builtins/io/filemtime.rs @@ -15,7 +15,9 @@ builtin! { name: "filemtime", area: Io, params: [filename: Str], - returns: Int, + // php returns int|false: false when the path cannot be stat'ed. Declaring Int + // discarded the false and handed back the raw payload register. + returns: Mixed, semantics: crate::builtins::semantics::runtime_fn_semantics( crate::ir::RuntimeFnId::Filemtime, ), diff --git a/src/builtins/io/filesize.rs b/src/builtins/io/filesize.rs index 8516d230e8..2add4a42d8 100644 --- a/src/builtins/io/filesize.rs +++ b/src/builtins/io/filesize.rs @@ -15,7 +15,9 @@ builtin! { name: "filesize", area: Io, params: [filename: Str], - returns: Int, + // php returns int|false: false when the path cannot be stat'ed. Declaring Int + // discarded the false and handed back the raw payload register. + returns: Mixed, semantics: crate::builtins::semantics::runtime_fn_semantics( crate::ir::RuntimeFnId::Filesize, ), diff --git a/src/builtins/io/fsockopen.rs b/src/builtins/io/fsockopen.rs index 17c4560a69..258eb42124 100644 --- a/src/builtins/io/fsockopen.rs +++ b/src/builtins/io/fsockopen.rs @@ -29,6 +29,7 @@ builtin! { semantics: crate::builtins::semantics::runtime_fn_semantics( crate::ir::RuntimeFnId::Fsockopen, ), + requirements: crate::builtins::semantics::stream_socket_client_requirements, summary: "Open Internet or Unix domain socket connection.", php_manual: "function.fsockopen", } diff --git a/src/builtins/io/lchgrp.rs b/src/builtins/io/lchgrp.rs index 6a2646d53c..ff47244cac 100644 --- a/src/builtins/io/lchgrp.rs +++ b/src/builtins/io/lchgrp.rs @@ -15,7 +15,7 @@ use crate::types::PhpType; builtin! { name: "lchgrp", area: Io, - params: [filename: Str, group: Str], + params: [filename: Str, group: Mixed], returns: Bool, check: check, semantics: crate::builtins::semantics::runtime_fn_semantics( diff --git a/src/builtins/io/lchown.rs b/src/builtins/io/lchown.rs index cff001d22b..a3bd1ed0e5 100644 --- a/src/builtins/io/lchown.rs +++ b/src/builtins/io/lchown.rs @@ -15,7 +15,7 @@ use crate::types::PhpType; builtin! { name: "lchown", area: Io, - params: [filename: Str, user: Str], + params: [filename: Str, user: Mixed], returns: Bool, check: check, semantics: crate::builtins::semantics::runtime_fn_semantics( diff --git a/src/builtins/io/mod.rs b/src/builtins/io/mod.rs index 53e1063c02..908c9bac64 100644 --- a/src/builtins/io/mod.rs +++ b/src/builtins/io/mod.rs @@ -22,7 +22,7 @@ //! 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), +//! - Library-linking check hooks: file_get_contents (TLS / pure-Rust PHAR), //! 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). @@ -135,6 +135,10 @@ pub mod pclose; pub mod pfsockopen; pub mod popen; pub mod print_r; +pub mod proc_close; +pub mod proc_get_status; +pub mod proc_open; +pub mod proc_terminate; pub mod readdir; pub mod readfile; pub mod readline; diff --git a/src/builtins/io/proc_close.rs b/src/builtins/io/proc_close.rs new file mode 100644 index 0000000000..a102c8fcae --- /dev/null +++ b/src/builtins/io/proc_close.rs @@ -0,0 +1,23 @@ +//! Purpose: +//! Declares PHP's `proc_close` builtin for the typed EIR runtime-call path. +//! +//! Called from: +//! - The builtin registry, type checker, and EIR runtime-call dispatcher. +//! +//! Key details: +//! - The target runtime closes registered parent pipes before waiting, avoiding +//! deadlock when an unread child pipe reaches capacity. +//! - The lowerer consumes the kind-5 process resource and stamps its release +//! sentinel so scope cleanup cannot wait or close the process twice. + +builtin! { + name: "proc_close", + area: Io, + params: [process: Mixed], + returns: Int, + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::ProcClose, + ), + summary: "Close a process opened by proc_open and return the exit status.", + php_manual: "function.proc-close", +} diff --git a/src/builtins/io/proc_get_status.rs b/src/builtins/io/proc_get_status.rs new file mode 100644 index 0000000000..c6ac14932b --- /dev/null +++ b/src/builtins/io/proc_get_status.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Home of the PHP `proc_get_status` builtin and its result-type contract. +//! +//! Called from: +//! - The builtin registry, type checker, and typed EIR runtime-call lowering. +//! +//! Key details: +//! - Windows reads the retained process HANDLE without consuming it, preserving +//! PHP's repeated-status semantics until `proc_close()` closes the resource. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::errors::CompileError; +use crate::types::PhpType; + +builtin! { + name: "proc_get_status", + area: Io, + params: [process: Mixed], + returns: Mixed, + check: check, + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::ProcGetStatus, + ), + summary: "Retrieves the current status of a process opened by proc_open.", + php_manual: "function.proc-get-status", +} + +/// Types the status record as PHP's string-keyed mixed associative array or false. +/// +/// The builtin registry has already inferred the process argument before it +/// calls this non-lazy hook, so this only refines the return shape. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(_cx.checker.normalize_union_type(vec![ + PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Mixed), + }, + PhpType::Bool, + ])) +} diff --git a/src/builtins/io/proc_open.rs b/src/builtins/io/proc_open.rs new file mode 100644 index 0000000000..69062ac5f3 --- /dev/null +++ b/src/builtins/io/proc_open.rs @@ -0,0 +1,650 @@ +//! Purpose: +//! Home of the PHP `proc_open` 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, False)` to reflect PHP's false-on-failure. +//! - The public parameter order and names match PHP: command, descriptor spec, +//! by-ref pipes, then optional cwd, environment and options. +//! - Windows accepts computed command arrays, cwd strings, environment maps, and +//! PHP's five documented process options; the backend marshals their runtime +//! storage before calling `CreateProcessW`. +//! - The Windows descriptor runtime supports pipes, sockets, files, redirects, +//! null devices, and supplied stream resources while preserving sparse keys. +//! - `TypeSpec` cannot express the command string|array union or nullable array +//! settings, so those parameters and the array parameters are declared Mixed +//! and refined by `check`. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::platform::Platform; +use crate::errors::CompileError; +use crate::parser::ast::{Expr, ExprKind}; +use crate::types::PhpType; + +/// Low-bit mask carried beside a marshalled Windows environment block. +/// +/// The remaining high bits hold the byte length of the optional environment +/// block. Keep this in one place so AST lowering, EIR lowering, and the native +/// runtime cannot disagree about the packed ABI. +pub(crate) const WINDOWS_PROC_OPTION_BITS: i64 = 5; + +/// Bypasses `cmd.exe` when the command is a string. +pub(crate) const WINDOWS_PROC_BYPASS_SHELL: i64 = 1; +/// Temporarily suppresses Win32 critical-error dialogs around process creation. +pub(crate) const WINDOWS_PROC_SUPPRESS_ERRORS: i64 = 1 << 1; +/// Requests PHP's Windows blocking-pipe mode. +pub(crate) const WINDOWS_PROC_BLOCKING_PIPES: i64 = 1 << 2; +/// Requests `CREATE_NEW_PROCESS_GROUP`. +pub(crate) const WINDOWS_PROC_CREATE_PROCESS_GROUP: i64 = 1 << 3; +/// Requests `CREATE_NEW_CONSOLE`. +pub(crate) const WINDOWS_PROC_CREATE_NEW_CONSOLE: i64 = 1 << 4; + +builtin! { + name: "proc_open", + area: Io, + params: [ + command: Mixed, + descriptor_spec: Mixed, + ref pipes: Mixed, + cwd: Mixed = DefaultSpec::Null, + env_vars: Mixed = DefaultSpec::Null, + options: Mixed = DefaultSpec::Null + ], + returns: Mixed, + check: check, + semantics: crate::builtins::semantics::with_argument_lowering( + crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::ProcOpen, + ), + crate::builtins::semantics::BuiltinArgumentLowering::ProcOpen, + ), + summary: "Execute a command and open file pointers for process I/O.", + php_manual: "function.proc-open", +} + +/// Returns `Union(stream_resource, False)` for the proc_open result. +/// +/// Windows accepts both string and scalar-array commands plus computed +/// associative arrays for environment/options. Other targets retain the existing +/// pipe runtime's narrower optional-setting support and diagnose Windows-only use. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let command_arg = argument_at(cx.args, 0, "command").expect("arity guarantees command"); + let command = cx.checker.infer_type(command_arg, cx.env)?; + match command { + PhpType::Str => {} + ty if valid_command_array_type(&ty) => { + if cx.checker.target_platform != Platform::Windows { + return Err(CompileError::new( + cx.span, + "proc_open() array commands are currently supported only for the Windows target", + )); + } + } + PhpType::Array(_) | PhpType::AssocArray { .. } => { + return Err(CompileError::new( + cx.span, + "proc_open() command array must contain only scalar values", + )); + } + _ => { + return Err(CompileError::new( + cx.span, + "proc_open() first argument must be string or array", + )); + } + } + + for (index, name) in [(1, "descriptor_spec"), (2, "pipes")] { + let arg = argument_at(cx.args, index, name).expect("arity guarantees required argument"); + let ty = cx.checker.infer_type(arg, cx.env)?; + if !matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new( + cx.span, + &format!("proc_open() ${name} argument must be array"), + )); + } + } + + if let Some(arg) = argument_at(cx.args, 3, "cwd") { + let ty = cx.checker.infer_type(arg, cx.env)?; + if !matches!(&ty, PhpType::Void | PhpType::Never) { + if cx.checker.target_platform != Platform::Windows { + return Err(CompileError::new( + cx.span, + "proc_open() non-null $cwd is currently supported only for the Windows target", + )); + } + if !matches!(ty, PhpType::Str) { + return Err(CompileError::new( + cx.span, + "proc_open() $cwd must be string or null", + )); + } + } + } + + if let Some(arg) = argument_at(cx.args, 4, "env_vars") { + let ty = cx.checker.infer_type(arg, cx.env)?; + if !matches!(&ty, PhpType::Void | PhpType::Never) { + if cx.checker.target_platform != Platform::Windows { + return Err(CompileError::new( + cx.span, + "proc_open() non-null $env_vars is currently supported only for the Windows target", + )); + } + if !valid_environment_type(&ty) { + return Err(CompileError::new( + cx.span, + "proc_open() $env_vars must be null or an array of scalar values", + )); + } + } + } + if let Some(arg) = argument_at(cx.args, 5, "options") { + let ty = cx.checker.infer_type(arg, cx.env)?; + if !matches!(&ty, PhpType::Void | PhpType::Never) { + if cx.checker.target_platform != Platform::Windows { + return Err(CompileError::new( + cx.span, + "proc_open() non-null $options is currently supported only for the Windows target", + )); + } + if !valid_options_type(&ty) { + return Err(CompileError::new( + cx.span, + "proc_open() $options must be an array", + )); + } + } + } + + Ok(cx.checker.normalize_union_type(vec![ + PhpType::stream_resource(), + PhpType::False, + ])) +} + +/// Returns whether a command array can be converted with PHP scalar-to-string rules. +fn valid_command_array_type(ty: &PhpType) -> bool { + match ty { + PhpType::Array(value) => valid_proc_scalar_type(value), + PhpType::AssocArray { key, value } => { + matches!(**key, PhpType::Str | PhpType::Int | PhpType::Mixed) + && valid_proc_scalar_type(value) + } + _ => false, + } +} + +/// Returns whether a value can be represented by the Windows process scalar converter. +fn valid_proc_scalar_type(ty: &PhpType) -> bool { + matches!( + ty, + PhpType::Str + | PhpType::Int + | PhpType::Float + | PhpType::Bool + | PhpType::False + | PhpType::Void + | PhpType::Mixed + ) +} + +/// Returns whether a checked Windows environment value can be marshalled by +/// the runtime scalar-to-string converter. +fn valid_environment_type(ty: &PhpType) -> bool { + match ty { + PhpType::Array(value) => valid_proc_scalar_type(value), + PhpType::AssocArray { key, value } => { + matches!(**key, PhpType::Str | PhpType::Int | PhpType::Mixed) + && valid_proc_scalar_type(value) + } + _ => false, + } +} + +/// Returns whether a checked options array can carry PHP's Windows option +/// values at runtime. +/// +/// php-src accepts truthy boolean and integer values for recognized keys, and +/// silently ignores unknown keys. The runtime mirrors that behavior; this +/// checker only rejects shapes that cannot be represented as an options array. +fn valid_options_type(ty: &PhpType) -> bool { + match ty { + PhpType::Array(value) => matches!( + **value, + PhpType::Bool | PhpType::False | PhpType::Int | PhpType::Mixed + ), + PhpType::AssocArray { key, value } => { + matches!(**key, PhpType::Str | PhpType::Mixed) + && matches!( + **value, + PhpType::Bool | PhpType::False | PhpType::Int | PhpType::Mixed + ) + } + _ => false, + } +} + +/// Returns one caller-visible argument by PHP parameter position, unwrapping +/// named arguments while retaining the positional-prefix rules checked earlier. +pub(crate) fn argument_at<'a>(args: &'a [Expr], index: usize, name: &str) -> Option<&'a Expr> { + let mut positional = 0usize; + for argument in args { + match &argument.kind { + ExprKind::NamedArg { + name: argument_name, + value, + } if argument_name == name => return Some(value), + ExprKind::NamedArg { .. } => {} + _ if positional == index => return Some(argument), + _ => positional += 1, + } + } + None +} + +/// Returns the literal command arguments after PHP scalar-to-string conversion +/// and ignores associative keys, as php-src's Windows command builder does. +pub(crate) fn static_command_array(expr: &Expr) -> Option> { + let values: Vec = match &expr.kind { + ExprKind::ArrayLiteral(items) => items + .iter() + .map(static_command_scalar) + .collect::>()?, + ExprKind::ArrayLiteralAssoc(entries) => entries + .iter() + .map(|(_, value)| static_command_scalar(value)) + .collect::>()?, + _ => return None, + }; + if values.first().is_some_and(|value| !value.is_empty()) { + Some(values) + } else { + None + } +} + +/// Converts one statically-known command value with PHP's scalar string rules. +fn static_command_scalar(expr: &Expr) -> Option { + match &expr.kind { + ExprKind::StringLiteral(value) => Some(value.clone()), + ExprKind::IntLiteral(value) => Some(value.to_string()), + ExprKind::FloatLiteral(value) => Some(value.to_string()), + ExprKind::BoolLiteral(value) => Some(if *value { "1" } else { "" }.to_string()), + ExprKind::Null => Some(String::new()), + _ => None, + } +} + +/// Returns literal environment entries in php-src's key/value form. +/// +/// `None` keys are numeric (or empty-string) hash keys, which php-src writes +/// as raw environment entries instead of synthesizing an `=` prefix. +pub(crate) fn static_environment(expr: &Expr) -> Option, String)>> { + let entries: Vec<(Option, &Expr)> = match &expr.kind { + ExprKind::ArrayLiteral(items) => items.iter().map(|value| (None, value)).collect(), + ExprKind::ArrayLiteralAssoc(entries) => entries + .iter() + .map(|(key, value)| { + let key = match &key.kind { + ExprKind::StringLiteral(key) if !key.contains('\0') && !key.is_empty() => { + Some(key.clone()) + } + ExprKind::StringLiteral(_) | ExprKind::IntLiteral(_) => None, + _ => return None, + }; + Some((key, value)) + }) + .collect::>()?, + _ => return None, + }; + let mut environment = Vec::with_capacity(entries.len()); + for (key, value) in entries { + let value = static_command_scalar(value)?; + if value.is_empty() { + continue; + } + if value.contains('\0') { + return None; + } + environment.push((key, value)); + } + Some(environment) +} + +/// Returns literal Windows option bits using php-src's truthiness rules. +/// +/// Unknown option keys are intentionally ignored, matching `get_option()` in +/// php-src. Dynamic literal values return `None` so EIR codegen can dispatch to +/// the runtime options marshaller instead of guessing their value. +pub(crate) fn static_windows_options(expr: &Expr) -> Option { + if matches!(&expr.kind, ExprKind::ArrayLiteral(items) if items.is_empty()) { + return Some(0); + } + let ExprKind::ArrayLiteralAssoc(entries) = &expr.kind else { + return None; + }; + let mut flags = 0; + for (key, value) in entries { + let ExprKind::StringLiteral(key) = &key.kind else { + return None; + }; + let Some(bit) = windows_option_bit(key) else { + continue; + }; + let enabled = match &value.kind { + ExprKind::BoolLiteral(value) => *value, + ExprKind::IntLiteral(value) => *value != 0, + _ => return None, + }; + if enabled { + flags |= bit; + } else { + flags &= !bit; + } + } + Some(flags) +} + +/// Maps one PHP Windows option name to its packed native ABI bit. +fn windows_option_bit(name: &str) -> Option { + match name { + "bypass_shell" => Some(WINDOWS_PROC_BYPASS_SHELL), + "suppress_errors" => Some(WINDOWS_PROC_SUPPRESS_ERRORS), + "blocking_pipes" => Some(WINDOWS_PROC_BLOCKING_PIPES), + "create_process_group" => Some(WINDOWS_PROC_CREATE_PROCESS_GROUP), + "create_new_console" => Some(WINDOWS_PROC_CREATE_NEW_CONSOLE), + _ => None, + } +} + +/// Quotes one argument according to the Windows `CommandLineToArgvW` backslash +/// and quote rules used for direct (array-form) process creation. +fn quote_windows_argument(argument: &str) -> String { + if !argument.is_empty() + && !argument + .chars() + .any(|character| character.is_whitespace() || character == '"') + { + return argument.to_string(); + } + let mut quoted = String::from("\""); + let mut backslashes = 0usize; + for character in argument.chars() { + if character == '\\' { + backslashes += 1; + continue; + } + if character == '"' { + quoted.extend(std::iter::repeat_n('\\', backslashes * 2 + 1)); + quoted.push('"'); + } else { + quoted.extend(std::iter::repeat_n('\\', backslashes)); + quoted.push(character); + } + backslashes = 0; + } + quoted.extend(std::iter::repeat_n('\\', backslashes * 2)); + quoted.push('"'); + quoted +} + +/// Returns whether a literal executable is handled by `cmd.exe` on Windows. +/// +/// php-src resolves short and full Windows paths before comparing the final +/// component. The AOT compiler deliberately uses the bounded source spelling +/// here: it strips slash-separated path components and compares `cmd`, +/// `cmd.exe`, `.bat`, and `.cmd` case-insensitively. Runtime array commands use +/// the identical bounded rule, so static and computed commands cannot diverge. +fn is_windows_cmd_program(program: &str) -> bool { + let basename = program.rsplit(['\\', '/']).next().unwrap_or(program); + basename.eq_ignore_ascii_case("cmd") + || basename.eq_ignore_ascii_case("cmd.exe") + || basename + .get(basename.len().saturating_sub(4)..) + .is_some_and(|extension| { + extension.eq_ignore_ascii_case(".bat") || extension.eq_ignore_ascii_case(".cmd") + }) +} + +/// Quotes a command-processor argument using php-src's caret escaping rules. +/// +/// Arguments for `cmd.exe` and batch files are always quoted. When they contain +/// a command-processor metacharacter, php-src also prefixes both surrounding +/// quotes and every metacharacter with `^`; retain the ordinary Windows +/// backslash-before-quote expansion underneath that extra protection. +fn quote_windows_cmd_argument(argument: &str) -> String { + const SPECIAL: &str = "()!^\"<>&|%"; + let has_special = argument.chars().any(|character| SPECIAL.contains(character)); + let mut quoted = String::new(); + if has_special { + quoted.push('^'); + } + quoted.push('"'); + let mut backslashes = 0usize; + for character in argument.chars() { + if character == '\\' { + backslashes += 1; + continue; + } + if character == '"' { + quoted.extend(std::iter::repeat_n('\\', backslashes * 2 + 1)); + } else { + quoted.extend(std::iter::repeat_n('\\', backslashes)); + } + if has_special && SPECIAL.contains(character) { + quoted.push('^'); + } + quoted.push(character); + backslashes = 0; + } + quoted.extend(std::iter::repeat_n('\\', backslashes * 2)); + if has_special { + quoted.push('^'); + } + quoted.push('"'); + quoted +} + +/// Builds the exact direct Windows command line for a validated literal argv. +pub(crate) fn static_windows_command_line(expr: &Expr) -> Option { + static_command_array(expr).map(|arguments| { + let cmd_execution = is_windows_cmd_program(&arguments[0]); + arguments + .iter() + .enumerate() + .map(|(index, argument)| { + if index != 0 && cmd_execution { + quote_windows_cmd_argument(argument) + } else { + quote_windows_argument(argument) + } + }) + .collect::>() + .join(" ") + }) +} + +/// Encodes a validated environment as the double-NUL-terminated UTF-8 block +/// consumed by the Windows runtime before its counted UTF-16 conversion. +pub(crate) fn static_windows_environment_block(expr: &Expr) -> Option { + let environment = static_environment(expr)?; + let mut block = String::new(); + for (name, value) in environment { + if let Some(name) = name { + block.push_str(&name); + block.push('='); + } + block.push_str(&value); + block.push('\0'); + } + if block.is_empty() { + block.push('\0'); + } + block.push('\0'); + Some(block) +} + +#[cfg(test)] +mod tests { + use super::{ + is_windows_cmd_program, quote_windows_argument, quote_windows_cmd_argument, + static_windows_command_line, static_windows_environment_block, static_windows_options, + WINDOWS_PROC_BLOCKING_PIPES, WINDOWS_PROC_BYPASS_SHELL, WINDOWS_PROC_CREATE_NEW_CONSOLE, + WINDOWS_PROC_SUPPRESS_ERRORS, + }; + use crate::lexer::tokenize; + use crate::parser::{ + ast::{ExprKind, StmtKind}, + parse, + }; + + /// Verifies Windows argv quoting preserves empty arguments, whitespace, + /// embedded quotes, and trailing backslashes. + #[test] + fn quotes_windows_process_arguments() { + assert_eq!(quote_windows_argument("plain"), "plain"); + assert_eq!(quote_windows_argument(""), "\"\""); + assert_eq!(quote_windows_argument("two words"), "\"two words\""); + assert_eq!(quote_windows_argument("a\"b"), "\"a\\\"b\""); + assert_eq!(quote_windows_argument("C:\\Program Files\\"), "\"C:\\Program Files\\\\\""); + } + + /// Verifies php-src's command-processor detection stays case-insensitive + /// while remaining bounded to literal executable spellings. + #[test] + fn detects_cmd_and_batch_program_names() { + assert!(is_windows_cmd_program("CMD.EXE")); + assert!(is_windows_cmd_program(r"C:\\Windows\\System32\\cmd")); + assert!(is_windows_cmd_program(r"tools\\BUILD.CmD")); + assert!(is_windows_cmd_program("run.BAT")); + assert!(!is_windows_cmd_program("cmd.exe.bak")); + assert!(!is_windows_cmd_program("acmd.exe")); + assert!(!is_windows_cmd_program("program.exe")); + } + + /// Verifies cmd.exe arguments escape every php-src metacharacter without + /// changing the ordinary executable quoting contract. + #[test] + fn quotes_windows_command_processor_arguments() { + assert_eq!(quote_windows_cmd_argument("plain"), "\"plain\""); + assert_eq!(quote_windows_cmd_argument("a&b"), "^\"a^&b^\""); + assert_eq!(quote_windows_cmd_argument("a\"&b"), "^\"a\\^\"^&b^\""); + assert_eq!(quote_windows_cmd_argument("a()!^\"<>&|%b"), "^\"a^(^)^!^^\\^\"^<^>^&^|^%b^\""); + } + + /// Verifies literal argv arrays apply php-src's cmd.exe escaping only after + /// the executable position, leaving ordinary executable arguments unchanged. + #[test] + fn static_command_lines_escape_only_command_processor_arguments() { + let program = parse(&tokenize( + r#" match &expr.kind { + ExprKind::FunctionCall { args, .. } => &args[0], + _ => panic!("fixture must be a call"), + }, + _ => panic!("fixture must be an expression statement"), + }; + assert_eq!( + static_windows_command_line(command), + Some("CMD.EXE \"/c\" ^\"echo a^&b^\"".to_string()) + ); + + let ordinary = parse(&tokenize( + r#" match &expr.kind { + ExprKind::FunctionCall { args, .. } => &args[0], + _ => panic!("fixture must be a call"), + }, + _ => panic!("fixture must be an expression statement"), + }; + assert_eq!( + static_windows_command_line(ordinary_command), + Some("tool.exe /c \"echo a&b\"".to_string()) + ); + } + + /// Verifies literal associative command arrays ignore keys and coerce scalar values. + #[test] + fn static_command_lines_ignore_keys_and_coerce_scalars() { + let program = parse(&tokenize( + r#" "tool.exe", 4 => 7, "empty" => false], [], $pipes);"#, + ) + .expect("scalar argv fixture tokenizes")) + .expect("scalar argv fixture parses"); + let command = match &program[0].kind { + StmtKind::ExprStmt(expr) => match &expr.kind { + ExprKind::FunctionCall { args, .. } => &args[0], + _ => panic!("fixture must be a call"), + }, + _ => panic!("fixture must be an expression statement"), + }; + assert_eq!( + static_windows_command_line(command), + Some("tool.exe 7 \"\"".to_string()) + ); + } + + /// Verifies literal environment arrays preserve php-src raw numeric entries and omit empty values. + #[test] + fn static_environment_block_keeps_raw_numeric_entries() { + let program = parse(&tokenize( + r#" "RAW=1", "=C:" => "C:\\tmp", "DROP" => false]);"#, + ) + .expect("environment fixture tokenizes")) + .expect("environment fixture parses"); + let environment = match &program[0].kind { + StmtKind::ExprStmt(expr) => match &expr.kind { + ExprKind::FunctionCall { args, .. } => &args[4], + _ => panic!("fixture must be a call"), + }, + _ => panic!("fixture must be an expression statement"), + }; + assert_eq!( + static_windows_environment_block(environment), + Some("RAW=1\0=C:=C:\\tmp\0\0".to_string()) + ); + } + + /// Verifies literal Windows options preserve php-src's recognized-key and + /// last-value semantics while ignoring unknown options. + #[test] + fn packs_all_supported_windows_options() { + let program = parse(&tokenize( + r#" true, + "suppress_errors" => 1, + "blocking_pipes" => false, + "create_new_console" => true, + "unknown" => true, + "bypass_shell" => false, + ]);"#, + ) + .expect("options fixture tokenizes")) + .expect("options fixture parses"); + let options = match &program[0].kind { + StmtKind::ExprStmt(expr) => match &expr.kind { + ExprKind::FunctionCall { args, .. } => &args[5], + _ => panic!("fixture must be a call"), + }, + _ => panic!("fixture must be an expression statement"), + }; + assert_eq!( + static_windows_options(options), + Some(WINDOWS_PROC_SUPPRESS_ERRORS | WINDOWS_PROC_CREATE_NEW_CONSOLE) + ); + assert_eq!(WINDOWS_PROC_BYPASS_SHELL | WINDOWS_PROC_BLOCKING_PIPES, 5); + } +} diff --git a/src/builtins/io/proc_terminate.rs b/src/builtins/io/proc_terminate.rs new file mode 100644 index 0000000000..917d883d3c --- /dev/null +++ b/src/builtins/io/proc_terminate.rs @@ -0,0 +1,124 @@ +//! Purpose: +//! Home of the PHP `proc_terminate` builtin and its signal argument validation. +//! +//! Called from: +//! - The builtin registry, checker, and typed EIR runtime-call lowering. +//! +//! Key details: +//! - Weak PHP scalar coercion accepts numeric strings, floats, booleans, and +//! null for the integer signal while rejecting arrays and objects. +//! - Windows intentionally ignores the optional signal and follows php-src by +//! terminating the retained process HANDLE with exit code 255. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::errors::CompileError; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "proc_terminate", + area: Io, + params: [process: Mixed, signal: Int = DefaultSpec::Int(15)], + returns: Bool, + check: check, + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::ProcTerminate, + ), + summary: "Terminates a process opened by proc_open.", + php_manual: "function.proc-terminate", +} + +/// Validates PHP's weak scalar-to-int signal surface and returns `Bool`. +/// +/// The registry pre-infers regular arguments. The hook re-infers the optional +/// signal to obtain its resolved type. Numeric string literals are checked +/// eagerly, while other weakly coercible scalar types are converted by codegen. +/// The process remains `Mixed` because resources have no finer type in the +/// current checker. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + if let Some(signal) = cx.args.get(1) { + let signal_ty = cx.checker.infer_type(signal, cx.env)?; + let invalid_string_literal = matches!( + &signal.kind, + ExprKind::StringLiteral(value) if !is_php_int_parameter_string(value) + ); + if invalid_string_literal || !accepts_weak_int_signal(&signal_ty) { + return Err(CompileError::new( + signal.span, + "proc_terminate() signal must be int", + )); + } + } + Ok(PhpType::Bool) +} + +/// Returns whether a statically inferred type can use PHP's weak `int` parameter coercion. +fn accepts_weak_int_signal(ty: &PhpType) -> bool { + match ty { + PhpType::Int + | PhpType::Float + | PhpType::Bool + | PhpType::False + | PhpType::Str + | PhpType::TaggedScalar + | PhpType::Void + | PhpType::Never => true, + PhpType::Union(members) => { + ty.codegen_repr() == PhpType::TaggedScalar + && members.iter().all(accepts_weak_int_signal) + } + _ => false, + } +} + +/// Recognizes PHP numeric strings accepted by a weakly typed `int` parameter. +fn is_php_int_parameter_string(value: &str) -> bool { + let bytes = value.as_bytes(); + let mut cursor = 0; + while cursor < bytes.len() && is_php_numeric_whitespace(bytes[cursor]) { + cursor += 1; + } + if cursor < bytes.len() && matches!(bytes[cursor], b'+' | b'-') { + cursor += 1; + } + + let mut digits = 0; + while cursor < bytes.len() && bytes[cursor].is_ascii_digit() { + cursor += 1; + digits += 1; + } + if cursor < bytes.len() && bytes[cursor] == b'.' { + cursor += 1; + while cursor < bytes.len() && bytes[cursor].is_ascii_digit() { + cursor += 1; + digits += 1; + } + } + if digits == 0 { + return false; + } + + if cursor < bytes.len() && matches!(bytes[cursor], b'e' | b'E') { + cursor += 1; + if cursor < bytes.len() && matches!(bytes[cursor], b'+' | b'-') { + cursor += 1; + } + let exponent_start = cursor; + while cursor < bytes.len() && bytes[cursor].is_ascii_digit() { + cursor += 1; + } + if cursor == exponent_start { + return false; + } + } + + while cursor < bytes.len() && is_php_numeric_whitespace(bytes[cursor]) { + cursor += 1; + } + cursor == bytes.len() +} + +/// Returns whether a byte is whitespace accepted around a PHP numeric string. +fn is_php_numeric_whitespace(byte: u8) -> bool { + byte == b' ' || (b'\t'..=b'\r').contains(&byte) +} diff --git a/src/builtins/io/stream_filter_append.rs b/src/builtins/io/stream_filter_append.rs index 3dff869980..cb9eed048d 100644 --- a/src/builtins/io/stream_filter_append.rs +++ b/src/builtins/io/stream_filter_append.rs @@ -38,7 +38,7 @@ builtin! { /// 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 +/// system library: `z` for zlib filters, `iconv` (macOS and Windows) 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( diff --git a/src/builtins/io/stream_filter_prepend.rs b/src/builtins/io/stream_filter_prepend.rs index 75cff19100..94dfecad0a 100644 --- a/src/builtins/io/stream_filter_prepend.rs +++ b/src/builtins/io/stream_filter_prepend.rs @@ -38,7 +38,7 @@ builtin! { /// 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 +/// system library: `z` for zlib filters, `iconv` (macOS and Windows) 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( diff --git a/src/builtins/io/stream_socket_client.rs b/src/builtins/io/stream_socket_client.rs index 367e450c97..b29e7efa53 100644 --- a/src/builtins/io/stream_socket_client.rs +++ b/src/builtins/io/stream_socket_client.rs @@ -21,6 +21,7 @@ builtin! { semantics: crate::builtins::semantics::runtime_fn_semantics( crate::ir::RuntimeFnId::StreamSocketClient, ), + requirements: crate::builtins::semantics::stream_socket_client_requirements, summary: "Open Internet or Unix domain socket connection.", php_manual: "function.stream-socket-client", } diff --git a/src/builtins/io/stream_socket_enable_crypto.rs b/src/builtins/io/stream_socket_enable_crypto.rs index 1d1f6e908d..9c22305b66 100644 --- a/src/builtins/io/stream_socket_enable_crypto.rs +++ b/src/builtins/io/stream_socket_enable_crypto.rs @@ -6,6 +6,8 @@ //! //! Key details: //! - `check` validates arg[0] is a stream resource and requires the `elephc_tls` library. +//! - PHP returns `true` on completion, integer `0` while a nonblocking +//! handshake needs more I/O, and `false` on failure. //! - Arguments are pre-inferred by the registry before the hook runs. use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; @@ -21,7 +23,7 @@ builtin! { crypto_method: Mixed = DefaultSpec::Null, session_stream: Mixed = DefaultSpec::Null ], - returns: Bool, + returns: Mixed, check: check, semantics: crate::builtins::semantics::runtime_fn_semantics( crate::ir::RuntimeFnId::StreamSocketEnableCrypto, @@ -30,8 +32,9 @@ builtin! { php_manual: "function.stream-socket-enable-crypto", } -/// Validates arg[0] is a stream resource, links the TLS library, and returns `Bool`. +/// Validates arg[0] is a stream resource, links the TLS library, and returns PHP's +/// `true|0|false` handshake 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::Bool) + Ok(PhpType::Union(vec![PhpType::Bool, PhpType::Int])) } diff --git a/src/builtins/io/tempnam.rs b/src/builtins/io/tempnam.rs index 8db136b938..ef7f556cfb 100644 --- a/src/builtins/io/tempnam.rs +++ b/src/builtins/io/tempnam.rs @@ -6,20 +6,29 @@ //! `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`. +//! - `tempnam` returns either an owned string or `false` when both its requested +//! directory and platform fallback directory cannot create a temporary file. +//! - The registry common path infers the arguments and enforces the exactly-2- +//! argument arity before calling the check hook. +use crate::builtins::spec::BuiltinCheckCtx; +use crate::errors::CompileError; +use crate::types::PhpType; builtin! { name: "tempnam", area: Io, params: [directory: Str, prefix: Str], - returns: Str, + returns: Mixed, + check: check, semantics: crate::builtins::semantics::runtime_fn_semantics( crate::ir::RuntimeFnId::Tempnam, ), summary: "Creates a file with a unique filename.", php_manual: "function.tempnam", } + +/// Returns `string|false`, matching PHP when temporary-file creation fails. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::Union(vec![PhpType::Str, PhpType::False])) +} diff --git a/src/builtins/math/mod.rs b/src/builtins/math/mod.rs index 36aaa06857..c5c8d3cf88 100644 --- a/src/builtins/math/mod.rs +++ b/src/builtins/math/mod.rs @@ -40,6 +40,7 @@ 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; diff --git a/src/builtins/math/random_bytes.rs b/src/builtins/math/random_bytes.rs new file mode 100644 index 0000000000..10ac564cbe --- /dev/null +++ b/src/builtins/math/random_bytes.rs @@ -0,0 +1,52 @@ +//! Purpose: +//! Home of the PHP `random_bytes` builtin: its declaration and compile-time length guard. +//! +//! Called from: +//! - Checker, EIR, optimizer, ownership, and callable consumers through +//! `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::errors::CompileError; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "random_bytes", + area: Math, + params: [length: Int], + returns: Str, + check: check, + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::RandomBytes, + ), + 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) +} diff --git a/src/builtins/mod.rs b/src/builtins/mod.rs index c110f527f0..bed5458916 100644 --- a/src/builtins/mod.rs +++ b/src/builtins/mod.rs @@ -18,9 +18,16 @@ pub mod registry; pub mod docs; mod convert; mod requirements; +/// Shared scheme predicate: the codegen lowering and the builtin requirement must +/// agree on which stream addresses select a crypto transport. +pub(crate) use requirements::address_selects_tls_transport; mod array; mod callables; mod io; +pub(crate) use io::proc_open::{ + argument_at as proc_open_argument_at, static_windows_command_line, + static_windows_environment_block, static_windows_options, WINDOWS_PROC_OPTION_BITS, +}; mod string; mod math; mod spl; diff --git a/src/builtins/requirements.rs b/src/builtins/requirements.rs index 48c8ba9430..10acc93d38 100644 --- a/src/builtins/requirements.rs +++ b/src/builtins/requirements.rs @@ -20,6 +20,8 @@ pub enum BuiltinRequirement { SystemLibrary(&'static str), /// Link a macOS-only system library while Linux resolves the API from libc. MacOsLibrary(&'static str), + /// Link a Windows-only system library while Unix targets resolve the API elsewhere. + WindowsLibrary(&'static str), /// Enable a named runtime feature collected from the final EIR module. RuntimeFeature(&'static str), } @@ -62,6 +64,48 @@ pub fn file_get_contents_requirements( } } +/// Resolves the TLS bridge needed by `stream_socket_client()` from its address. +/// +/// Resolves the TLS bridge needed by `stream_socket_client()` and `fsockopen()`. +/// +/// php-src registers `ssl`, `sslv3`, `tls` and `tlsv1.0`..`tlsv1.3` as crypto +/// transports whose factory handshakes inside the connect +/// (`php_openssl_ssl_socket_factory`, ext/openssl/xp_ssl.c), so those addresses +/// need the bridge. A literal address settles it at compile time; a runtime string +/// could still carry a crypto scheme, so the bridge is linked conservatively -- +/// the same trade `file_get_contents_requirements` makes. +/// +/// The lowering skips the handshake path on exactly the same condition, reading +/// the literal back out of the `ConstStr` that defines the operand. The two must +/// stay in step: if this said "no bridge" while the lowering still emitted the +/// crypto path, the bridge exports would be undefined at link time. That failure +/// is loud and caught by CI, never a silent plaintext connection. +pub fn stream_socket_client_requirements( + input: &BuiltinRequirementInput<'_>, +) -> Vec { + match input.args.first().map(|arg| &arg.kind) { + Some(ExprKind::StringLiteral(address)) if !address_selects_tls_transport(address) => { + Vec::new() + } + _ => vec![BuiltinRequirement::Bridge("elephc_tls")], + } +} + +/// Reports whether a stream address names one of PHP's crypto transports. +/// +/// `sslv2` is deliberately excluded because php rejects it rather than +/// negotiating. Scheme names are case-insensitive. An address with no `://` is a +/// bare host, which `fsockopen()` accepts and which never selects a transport. +pub fn address_selects_tls_transport(address: &str) -> bool { + let Some((scheme, _)) = address.split_once("://") else { + return false; + }; + matches!( + scheme.to_ascii_lowercase().as_str(), + "ssl" | "sslv3" | "tls" | "tlsv1.0" | "tlsv1.1" | "tlsv1.2" | "tlsv1.3" + ) +} + /// Resolves PHAR libraries needed by `file_put_contents()` from its filename expression. pub fn file_put_contents_requirements( input: &BuiltinRequirementInput<'_>, @@ -128,7 +172,10 @@ pub fn stream_filter_requirements( if matches!(filter.as_str(), "zlib.deflate" | "zlib.inflate") { vec![BuiltinRequirement::SystemLibrary("z")] } else if filter.starts_with("convert.iconv.") { - vec![BuiltinRequirement::MacOsLibrary("iconv")] + vec![ + BuiltinRequirement::MacOsLibrary("iconv"), + BuiltinRequirement::WindowsLibrary("iconv"), + ] } else if matches!(filter.as_str(), "bzip2.compress" | "bzip2.decompress") { vec![BuiltinRequirement::SystemLibrary("bz2")] } else { @@ -136,6 +183,13 @@ pub fn stream_filter_requirements( } } +/// Requires the timezone bridge only on Windows, where the CRT lacks IANA zone data. +pub fn windows_timezone_requirements( + _input: &BuiltinRequirementInput<'_>, +) -> Vec { + vec![BuiltinRequirement::WindowsLibrary("elephc_tz")] +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/builtins/semantics.rs b/src/builtins/semantics.rs index e4c6ed4b55..3e0310da9a 100644 --- a/src/builtins/semantics.rs +++ b/src/builtins/semantics.rs @@ -22,8 +22,9 @@ use crate::types::PhpType; pub use crate::builtins::requirements::{ file_get_contents_requirements, file_put_contents_requirements, fopen_requirements, - stream_filter_requirements, unlink_requirements, BuiltinRequirement, - BuiltinRequirementInput, BuiltinRequirements, RequirementsFn, + stream_socket_client_requirements, + stream_filter_requirements, unlink_requirements, windows_timezone_requirements, + BuiltinRequirement, BuiltinRequirementInput, BuiltinRequirements, RequirementsFn, }; /// Inputs shared by backend-neutral validation, result-type, and effect resolvers. @@ -155,6 +156,8 @@ pub enum BuiltinArgumentLowering { Date, /// Preserve JSON decode's source-sensitive option handling. JsonDecode, + /// Marshal Windows `proc_open` command and environment extensions. + ProcOpen, /// Lower a statically known callback descriptor before its subject. PregReplaceCallback, /// Keep positional regex operands in raw source order. diff --git a/src/builtins/string/escapeshellarg.rs b/src/builtins/string/escapeshellarg.rs new file mode 100644 index 0000000000..2fa3a46bfa --- /dev/null +++ b/src/builtins/string/escapeshellarg.rs @@ -0,0 +1,74 @@ +//! Purpose: +//! Declares PHP's `escapeshellarg` builtin and its typed shell-escaping runtime target. +//! +//! Called from: +//! - The builtin registry, checker, optimizer, and AST-to-EIR builtin lowering path. +//! +//! Key details: +//! - The helper is platform-aware: POSIX single-quotes arguments and Windows uses PHP's +//! double-quoted command-line rule. +//! - Embedded NUL bytes raise PHP's catchable `ValueError`, so the operation may throw. + +use crate::{ + builtins::spec::BuiltinCheckCtx, + errors::CompileError, + ir::{RuntimeCallTarget, UnaryStringRuntime}, + types::PhpType, +}; + +builtin! { + name: "escapeshellarg", + area: String, + params: [arg: Str], + returns: Str, + check: check, + lazy_check: true, + semantics: crate::builtins::semantics::unary_string_runtime( + RuntimeCallTarget::UnaryString(UnaryStringRuntime::EscapeShellArg), + crate::ir::Effects::MAY_THROW, + ), + summary: "Quotes one argument for safe use in a shell command.", + php_manual: "https://www.php.net/manual/en/function.escapeshellarg.php", +} + +/// Accepts PHP's weak scalar string coercions while rejecting compound and object inputs. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !accepts_weak_scalar_string(&ty) { + return Err(CompileError::new( + cx.args[0].span, + "escapeshellarg() argument #1 ($arg) must be string", + )); + } + + Ok(PhpType::Str) +} + +/// Returns whether a statically known type can use PHP's weak scalar-to-string conversion. +/// +/// `Mixed` remains deliberately rejected: its boxed payload can contain an array or object, and +/// generic mixed string casting cannot provide this builtin's TypeError contract. `TaggedScalar` +/// is safe because it is the codegen-only `int|null` representation (see `PhpType`). +fn accepts_weak_scalar_string(ty: &PhpType) -> bool { + match ty { + PhpType::Str + | PhpType::Int + | PhpType::Float + | PhpType::Bool + | PhpType::False + | PhpType::Void + | PhpType::Never + | PhpType::TaggedScalar => true, + PhpType::Union(members) => members.iter().all(accepts_weak_scalar_string), + PhpType::Mixed + | PhpType::Array(_) + | PhpType::AssocArray { .. } + | PhpType::Buffer(_) + | PhpType::Iterable + | PhpType::Callable + | PhpType::Object(_) + | PhpType::Packed(_) + | PhpType::Pointer(_) + | PhpType::Resource(_) => false, + } +} diff --git a/src/builtins/string/escapeshellcmd.rs b/src/builtins/string/escapeshellcmd.rs new file mode 100644 index 0000000000..da47634edd --- /dev/null +++ b/src/builtins/string/escapeshellcmd.rs @@ -0,0 +1,73 @@ +//! Purpose: +//! Declares PHP's `escapeshellcmd` builtin and its typed shell-command runtime target. +//! +//! Called from: +//! - The builtin registry, checker, optimizer, and AST-to-EIR builtin lowering path. +//! +//! Key details: +//! - The helper follows PHP's platform-specific shell metacharacter escaping rules. +//! - Embedded NUL bytes raise PHP's catchable `ValueError`, so the operation may throw. + +use crate::{ + builtins::spec::BuiltinCheckCtx, + errors::CompileError, + ir::{RuntimeCallTarget, UnaryStringRuntime}, + types::PhpType, +}; + +builtin! { + name: "escapeshellcmd", + area: String, + params: [command: Str], + returns: Str, + check: check, + lazy_check: true, + semantics: crate::builtins::semantics::unary_string_runtime( + RuntimeCallTarget::UnaryString(UnaryStringRuntime::EscapeShellCmd), + crate::ir::Effects::MAY_THROW, + ), + summary: "Escapes shell metacharacters in a command string.", + php_manual: "https://www.php.net/manual/en/function.escapeshellcmd.php", +} + +/// Accepts PHP's weak scalar string coercions while rejecting compound and object commands. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !accepts_weak_scalar_string(&ty) { + return Err(CompileError::new( + cx.args[0].span, + "escapeshellcmd() argument #1 ($command) must be string", + )); + } + + Ok(PhpType::Str) +} + +/// Returns whether a statically known type can use PHP's weak scalar-to-string conversion. +/// +/// `Mixed` remains deliberately rejected: its boxed payload can contain an array or object, and +/// generic mixed string casting cannot provide this builtin's TypeError contract. `TaggedScalar` +/// is safe because it is the codegen-only `int|null` representation (see `PhpType`). +fn accepts_weak_scalar_string(ty: &PhpType) -> bool { + match ty { + PhpType::Str + | PhpType::Int + | PhpType::Float + | PhpType::Bool + | PhpType::False + | PhpType::Void + | PhpType::Never + | PhpType::TaggedScalar => true, + PhpType::Union(members) => members.iter().all(accepts_weak_scalar_string), + PhpType::Mixed + | PhpType::Array(_) + | PhpType::AssocArray { .. } + | PhpType::Buffer(_) + | PhpType::Iterable + | PhpType::Callable + | PhpType::Object(_) + | PhpType::Packed(_) + | PhpType::Pointer(_) + | PhpType::Resource(_) => false, + } +} diff --git a/src/builtins/string/mb_strlen.rs b/src/builtins/string/mb_strlen.rs index 27f7c69296..152b303442 100644 --- a/src/builtins/string/mb_strlen.rs +++ b/src/builtins/string/mb_strlen.rs @@ -6,7 +6,8 @@ //! `crate::builtins::registry`. //! //! Key details: -//! - The public signature matches PHP: `mb_strlen(string $string, ?string $encoding = null)`. +//! - The checker enforces PHP's `?string $encoding = null` contract while the registry keeps +//! the concrete argument representation intact for the target runtime. //! - Omitted/null encoding uses UTF-8; explicit encodings are handled by the target runtime, //! which keeps malformed-sequence counting aligned with mbstring and rejects unknown names. @@ -19,7 +20,7 @@ use crate::{ builtin! { name: "mb_strlen", area: String, - params: [string: Str, encoding: Str = DefaultSpec::Null], + params: [string: Str, encoding: Mixed = DefaultSpec::Null], returns: Int, check: check, lazy_check: true, diff --git a/src/builtins/string/mod.rs b/src/builtins/string/mod.rs index 29ddfecb2c..f79b3efed8 100644 --- a/src/builtins/string/mod.rs +++ b/src/builtins/string/mod.rs @@ -22,6 +22,8 @@ pub mod ctype_alnum; pub mod ctype_alpha; pub mod ctype_digit; pub mod ctype_space; +pub mod escapeshellarg; +pub mod escapeshellcmd; pub mod explode; pub mod grapheme_strrev; pub mod gzcompress; diff --git a/src/builtins/system/__elephc_strtotime_raw.rs b/src/builtins/system/__elephc_strtotime_raw.rs index 0f1b103d24..eeeb984b62 100644 --- a/src/builtins/system/__elephc_strtotime_raw.rs +++ b/src/builtins/system/__elephc_strtotime_raw.rs @@ -21,6 +21,7 @@ builtin! { semantics: crate::builtins::semantics::runtime_fn_semantics( crate::ir::RuntimeFnId::ElephcStrtotimeRaw, ), + requirements: crate::builtins::semantics::windows_timezone_requirements, summary: "Internal raw strtotime alias returning a plain integer.", internal: true, } diff --git a/src/builtins/system/date.rs b/src/builtins/system/date.rs index c59ef3d5ec..cfb55ca14e 100644 --- a/src/builtins/system/date.rs +++ b/src/builtins/system/date.rs @@ -21,5 +21,6 @@ builtin! { crate::builtins::semantics::runtime_fn_semantics(crate::ir::RuntimeFnId::Date), crate::builtins::semantics::BuiltinArgumentLowering::Date, ), + requirements: crate::builtins::semantics::windows_timezone_requirements, summary: "Formats a local time/date.", } diff --git a/src/builtins/system/gmdate.rs b/src/builtins/system/gmdate.rs index acc0a54419..8f8cb6dced 100644 --- a/src/builtins/system/gmdate.rs +++ b/src/builtins/system/gmdate.rs @@ -20,5 +20,6 @@ builtin! { semantics: crate::builtins::semantics::runtime_fn_semantics( crate::ir::RuntimeFnId::Gmdate, ), + requirements: crate::builtins::semantics::windows_timezone_requirements, summary: "Formats a GMT/UTC date and time.", } diff --git a/src/builtins/system/gmmktime.rs b/src/builtins/system/gmmktime.rs index 2688f29bbe..d7d0bdab1e 100644 --- a/src/builtins/system/gmmktime.rs +++ b/src/builtins/system/gmmktime.rs @@ -18,5 +18,6 @@ builtin! { semantics: crate::builtins::semantics::runtime_fn_semantics( crate::ir::RuntimeFnId::Gmmktime, ), + requirements: crate::builtins::semantics::windows_timezone_requirements, summary: "Returns the Unix timestamp for a GMT date.", } diff --git a/src/builtins/system/mktime.rs b/src/builtins/system/mktime.rs index 437dcdac21..0189df3e4a 100644 --- a/src/builtins/system/mktime.rs +++ b/src/builtins/system/mktime.rs @@ -18,5 +18,6 @@ builtin! { semantics: crate::builtins::semantics::runtime_fn_semantics( crate::ir::RuntimeFnId::Mktime, ), + requirements: crate::builtins::semantics::windows_timezone_requirements, summary: "Returns the Unix timestamp for a date.", } diff --git a/src/builtins/system/strtotime.rs b/src/builtins/system/strtotime.rs index 01b848b702..0b2fa7f2c2 100644 --- a/src/builtins/system/strtotime.rs +++ b/src/builtins/system/strtotime.rs @@ -21,6 +21,7 @@ builtin! { semantics: crate::builtins::semantics::runtime_fn_semantics( crate::ir::RuntimeFnId::Strtotime, ), + requirements: crate::builtins::semantics::windows_timezone_requirements, summary: "Parses an English textual datetime description into a Unix timestamp.", } diff --git a/src/codegen/block_emit.rs b/src/codegen/block_emit.rs index a144a60742..2fab7608c9 100644 --- a/src/codegen/block_emit.rs +++ b/src/codegen/block_emit.rs @@ -708,13 +708,9 @@ fn emit_generator_callback( } } let overflow_bytes = abi::materialize_outgoing_args(emitter, &assignments); - // ARM64 keeps a 16-byte nested-call save slot below the outgoing stack arguments; - // x86_64 needs no such pad because the call instruction itself pushes the return address. - let call_pad = if overflow_bytes > 0 && target.arch == Arch::AArch64 { - 16 - } else { - 0 - }; + // Preserve the target's caller-side stack contract: AArch64 needs its nested-call + // save slot when arguments overflow, while Windows x86_64 always needs shadow space. + let call_pad = abi::outgoing_call_stack_pad_bytes(target, overflow_bytes); abi::emit_reserve_temporary_stack(emitter, call_pad); // -- run the body, park its return value, and yield a null fiber transfer value -- @@ -726,7 +722,7 @@ fn emit_generator_callback( 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 + abi::emit_release_temporary_stack(emitter, call_pad); // drop the target-specific nested-call padding abi::emit_release_temporary_stack(emitter, overflow_bytes); // drop any stack-passed parameters after the body returns match target.arch { Arch::AArch64 => { diff --git a/src/codegen/context.rs b/src/codegen/context.rs index c690168b80..c80aebf102 100644 --- a/src/codegen/context.rs +++ b/src/codegen/context.rs @@ -632,7 +632,7 @@ impl<'a> FunctionContext<'a> { && matches!(source_ty.codegen_repr(), PhpType::Mixed) { let result_reg = abi::int_result_reg(self.emitter); - let arg_reg = abi::int_arg_reg_name(self.emitter.target, 0); + let arg_reg = abi::runtime_helper_int_arg_reg(self.emitter, 0); if result_reg != arg_reg { abi::emit_reg_move(self.emitter, arg_reg, result_reg); } diff --git a/src/codegen/eval_callable_helpers.rs b/src/codegen/eval_callable_helpers.rs index 536e1512e9..98221d3d0d 100644 --- a/src/codegen/eval_callable_helpers.rs +++ b/src/codegen/eval_callable_helpers.rs @@ -311,8 +311,14 @@ fn emit_x86_64_eval_dynamic_callable_invoker( emitter.instruction("push rbp"); // preserve the caller frame pointer emitter.instruction("mov rbp, rsp"); // establish a stable invoker frame pointer emitter.instruction("sub rsp, 64"); // reserve descriptor, arg-array, and eval result slots - emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the runtime callable descriptor pointer - emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the boxed Mixed invoker argument array + emitter.instruction(&format!( // save the runtime callable descriptor pointer + "mov QWORD PTR [rbp - 8], {}", + abi::int_arg_reg_name(emitter.target, 0) + )); + emitter.instruction(&format!( // save the boxed Mixed invoker argument array + "mov QWORD PTR [rbp - 16], {}", + abi::int_arg_reg_name(emitter.target, 1) + )); emitter.instruction("mov QWORD PTR [rbp - 48], 0"); // clear result kind and padding before the FFI call emitter.instruction("mov QWORD PTR [rbp - 40], 0"); // clear result value pointer before the FFI call emitter.instruction("mov QWORD PTR [rbp - 32], 0"); // clear result error pointer before the FFI call @@ -327,7 +333,12 @@ fn emit_x86_64_eval_dynamic_callable_invoker( )); // pass the captured eval callback as FFI argument 2 emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // pass the boxed Mixed invoker argument array emitter.instruction("lea rcx, [rbp - 48]"); // pass writable eval result storage - abi::emit_call_label(emitter, &symbol); + if module.target.platform == crate::codegen::platform::Platform::Windows { + abi::emit_symbol_address(emitter, "r11", &symbol); + emitter.emit_native_bridge_call("r11", 4); // enter magician through the MSx64 Rust ABI + } else { + abi::emit_call_label(emitter, &symbol); + } emitter.instruction("test eax, eax"); // did magician complete callback dispatch successfully? emitter.instruction(&format!("je {}", done_label)); // return the boxed result on success emitter.instruction("cmp eax, 3"); // status 3 means an eval Throwable escaped @@ -802,8 +813,11 @@ fn emit_eval_instance_method_descriptor_entry_wrapper_body( let actual_assignments = abi::build_outgoing_arg_assignments_for_target(emitter.target, &actual_types, 0); let (incoming_stack_offsets, _) = descriptor_entry_stack_offsets(&incoming_assignments); - let (actual_stack_offsets, actual_overflow_bytes) = + let (mut actual_stack_offsets, actual_overflow_bytes) = descriptor_entry_stack_offsets(&actual_assignments); + let actual_call_pad_bytes = abi::outgoing_call_stack_pad_bytes(emitter.target, 0); + shift_descriptor_entry_stack_offsets(&mut actual_stack_offsets, actual_call_pad_bytes); + let actual_call_stack_bytes = actual_overflow_bytes + actual_call_pad_bytes; let frame_size = descriptor_entry_frame_size(incoming_types.len()); abi::emit_frame_prologue(emitter, frame_size); @@ -820,8 +834,8 @@ fn emit_eval_instance_method_descriptor_entry_wrapper_body( incoming_stack_offsets[idx], ); } - if actual_overflow_bytes > 0 { - abi::emit_reserve_temporary_stack(emitter, actual_overflow_bytes); + if actual_call_stack_bytes > 0 { + abi::emit_reserve_temporary_stack(emitter, actual_call_stack_bytes); } for (idx, (ty, assignment)) in actual_types .iter() @@ -842,8 +856,8 @@ fn emit_eval_instance_method_descriptor_entry_wrapper_body( ); } abi::emit_call_label(emitter, &method_symbol(class_name, method_key)); - if actual_overflow_bytes > 0 { - abi::emit_release_temporary_stack(emitter, actual_overflow_bytes); + if actual_call_stack_bytes > 0 { + abi::emit_release_temporary_stack(emitter, actual_call_stack_bytes); } abi::emit_frame_restore(emitter, frame_size); abi::emit_return(emitter); @@ -868,8 +882,11 @@ fn emit_eval_static_method_descriptor_entry_wrapper_body( let actual_assignments = abi::build_outgoing_arg_assignments_for_target(emitter.target, &actual_types, 0); let (incoming_stack_offsets, _) = descriptor_entry_stack_offsets(&incoming_assignments); - let (actual_stack_offsets, actual_overflow_bytes) = + let (mut actual_stack_offsets, actual_overflow_bytes) = descriptor_entry_stack_offsets(&actual_assignments); + let actual_call_pad_bytes = abi::outgoing_call_stack_pad_bytes(emitter.target, 0); + shift_descriptor_entry_stack_offsets(&mut actual_stack_offsets, actual_call_pad_bytes); + let actual_call_stack_bytes = actual_overflow_bytes + actual_call_pad_bytes; let frame_size = descriptor_entry_frame_size(visible_arg_types.len()); abi::emit_frame_prologue(emitter, frame_size); @@ -886,8 +903,8 @@ fn emit_eval_static_method_descriptor_entry_wrapper_body( incoming_stack_offsets[idx], ); } - if actual_overflow_bytes > 0 { - abi::emit_reserve_temporary_stack(emitter, actual_overflow_bytes); + if actual_call_stack_bytes > 0 { + abi::emit_reserve_temporary_stack(emitter, actual_call_stack_bytes); } for (idx, (ty, assignment)) in actual_types .iter() @@ -912,8 +929,8 @@ fn emit_eval_static_method_descriptor_entry_wrapper_body( } } abi::emit_call_label(emitter, &static_method_symbol(impl_class, method_key)); - if actual_overflow_bytes > 0 { - abi::emit_release_temporary_stack(emitter, actual_overflow_bytes); + if actual_call_stack_bytes > 0 { + abi::emit_release_temporary_stack(emitter, actual_call_stack_bytes); } abi::emit_frame_restore(emitter, frame_size); abi::emit_return(emitter); @@ -997,6 +1014,13 @@ fn descriptor_entry_stack_offsets( (offsets, next_offset) } +/// Shifts eval descriptor overflow arguments above the platform caller-stack prefix. +fn shift_descriptor_entry_stack_offsets(offsets: &mut [Option], prefix_bytes: usize) { + for offset in offsets.iter_mut().flatten() { + *offset += prefix_bytes; + } +} + /// Converts a descriptor overflow offset into a caller-stack frame offset. fn descriptor_entry_caller_stack_offset(emitter: &Emitter, stack_offset: usize) -> usize { let cursor = abi::IncomingArgCursor::for_target(emitter.target, 0); @@ -1385,7 +1409,12 @@ fn emit_x86_64_eval_dynamic_callable_descriptor( emitter.instruction("test rdi, rdi"); // check whether a context was passed by magician emitter.instruction(&format!("jz {}", fail_label)); // reject dynamic eval callables when no context is active emitter.instruction("mov rsi, QWORD PTR [rbp - 40]"); // pass the original boxed eval callback value - abi::emit_call_label(emitter, &is_callable_symbol); + if module.target.platform == crate::codegen::platform::Platform::Windows { + abi::emit_symbol_address(emitter, "r11", &is_callable_symbol); + emitter.emit_native_bridge_call("r11", 2); // validate through magician's MSx64 Rust ABI + } else { + abi::emit_call_label(emitter, &is_callable_symbol); + } emitter.instruction("test eax, eax"); // check whether magician accepts the callback value emitter.instruction(&format!("jz {}", fail_label)); // reject non-callable eval values emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed callback value to retain it @@ -1489,8 +1518,10 @@ fn emit_x86_64_eval_callable_array_selector_slots(module: &Module, emitter: &mut /// Loads and unboxes one ARM64 callable-array slot through eval's array reader. fn emit_aarch64_eval_callable_array_slot(module: &Module, emitter: &mut Emitter, index: usize) { - let value_int_symbol = module.target.extern_symbol("__elephc_eval_value_int"); - let array_get_symbol = module.target.extern_symbol("__elephc_eval_value_array_get"); + let value_int_symbol = + abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_int"); + let array_get_symbol = + abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_array_get"); abi::emit_load_int_immediate(emitter, "x0", index as i64); abi::emit_call_label(emitter, &value_int_symbol); emitter.instruction("str x0, [sp, #-16]!"); // preserve the boxed callable-array index @@ -1503,8 +1534,10 @@ fn emit_aarch64_eval_callable_array_slot(module: &Module, emitter: &mut Emitter, /// Loads and unboxes one x86_64 callable-array slot through eval's array reader. fn emit_x86_64_eval_callable_array_slot(module: &Module, emitter: &mut Emitter, index: usize) { - let value_int_symbol = module.target.extern_symbol("__elephc_eval_value_int"); - let array_get_symbol = module.target.extern_symbol("__elephc_eval_value_array_get"); + let value_int_symbol = + abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_int"); + let array_get_symbol = + abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_array_get"); abi::emit_load_int_immediate(emitter, "rdi", index as i64); abi::emit_call_label(emitter, &value_int_symbol); abi::emit_push_reg(emitter, "rax"); diff --git a/src/codegen/eval_class_constant_helpers.rs b/src/codegen/eval_class_constant_helpers.rs index 8e597c9a36..fe4fcaf381 100644 --- a/src/codegen/eval_class_constant_helpers.rs +++ b/src/codegen/eval_class_constant_helpers.rs @@ -1063,9 +1063,10 @@ fn emit_reflection_constant_names_aarch64( ) { let done_label = "__elephc_eval_reflection_constant_names_done"; let fail_label = "__elephc_eval_reflection_constant_names_fail"; - let array_new = emitter - .target - .extern_symbol("__elephc_eval_value_string_array_new"); + let array_new = abi::c_callback_internal_symbol( + emitter.target, + "__elephc_eval_value_string_array_new", + ); emitter.instruction("sub sp, sp, #64"); // reserve helper frame for class slice, array, and fp/lr emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the Rust caller frame across runtime calls emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer @@ -1096,9 +1097,10 @@ fn emit_reflection_constant_names_x86_64( ) { let done_label = "__elephc_eval_reflection_constant_names_done_x"; let fail_label = "__elephc_eval_reflection_constant_names_fail_x"; - let array_new = emitter - .target - .extern_symbol("__elephc_eval_value_string_array_new"); + let array_new = abi::c_callback_internal_symbol( + emitter.target, + "__elephc_eval_value_string_array_new", + ); emitter.instruction("push rbp"); // preserve the Rust caller frame pointer emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer emitter.instruction("sub rsp, 32"); // reserve aligned slots for class slice and result array @@ -1131,9 +1133,10 @@ fn emit_aarch64_constant_name_push( fail_label: &str, ) { let skip_label = format!("__elephc_eval_reflection_constant_names_skip_{}", index); - let push_symbol = emitter - .target - .extern_symbol("__elephc_eval_value_string_array_push"); + let push_symbol = abi::c_callback_internal_symbol( + emitter.target, + "__elephc_eval_value_string_array_push", + ); emit_aarch64_class_name_compare(emitter, data, &slot.reflected_class, &skip_label); let (label, len) = data.add_string(slot.constant.as_bytes()); emitter.instruction("ldr x0, [sp, #16]"); // reload the boxed result string array @@ -1154,9 +1157,10 @@ fn emit_x86_64_constant_name_push( fail_label: &str, ) { let skip_label = format!("__elephc_eval_reflection_constant_names_skip_{}_x", index); - let push_symbol = emitter - .target - .extern_symbol("__elephc_eval_value_string_array_push"); + let push_symbol = abi::c_callback_internal_symbol( + emitter.target, + "__elephc_eval_value_string_array_push", + ); emit_x86_64_class_name_compare(emitter, data, &slot.reflected_class, &skip_label); let (label, len) = data.add_string(slot.constant.as_bytes()); emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload the boxed result string array @@ -1514,6 +1518,6 @@ fn label_fragment(value: &str) -> String { /// Emits a C-global label using the target's symbol spelling. fn label_c_global(module: &Module, emitter: &mut Emitter, symbol: &str) { - let label = module.target.extern_symbol(symbol); - emitter.label_global(&label); + debug_assert_eq!(module.target, emitter.target); + abi::emit_c_callback_entry(emitter, symbol); } diff --git a/src/codegen/eval_constructor_helpers.rs b/src/codegen/eval_constructor_helpers.rs index 77a175ba68..f1bd6ff0f0 100644 --- a/src/codegen/eval_constructor_helpers.rs +++ b/src/codegen/eval_constructor_helpers.rs @@ -732,7 +732,8 @@ fn emit_aarch64_validate_builtin_throwable_arg_count( fail_label: &str, ) { emitter.instruction("ldr x0, [sp, #24]"); // reload the eval argument array for builtin Throwable arity validation - let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + let array_len_symbol = + abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_array_len"); abi::emit_call_label(emitter, &array_len_symbol); emitter.instruction("str x0, [sp, #40]"); // preserve argc outside the eval argument scratch slot emitter.instruction("cmp x0, #3"); // compact Throwable initialization supports message/code/previous @@ -746,7 +747,8 @@ fn emit_x86_64_validate_builtin_throwable_arg_count( fail_label: &str, ) { emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the eval argument array for builtin Throwable arity validation - let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + let array_len_symbol = + abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_array_len"); abi::emit_call_label(emitter, &array_len_symbol); emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save constructor argc for message/code initialization emitter.instruction("cmp rax, 3"); // compact Throwable initialization supports message/code/previous @@ -936,6 +938,9 @@ fn emit_x86_64_constructor_body( let receiver_ty = PhpType::Object(slot.class_name.clone()); let overflow_bytes = materialize_constructor_args(module, emitter, &receiver_ty, &slot.params, &slot.ref_params); + if slot.runtime_helper.is_some() { + abi::emit_windows_c_abi_registers_for_runtime_helper(emitter); + } let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); let callee = slot @@ -1033,7 +1038,8 @@ fn emit_aarch64_validate_constructor_arg_count( fail_label: &str, ) { emitter.instruction("ldr x0, [sp, #24]"); // reload the eval argument array for arity validation - let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + let array_len_symbol = + abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_array_len"); abi::emit_call_label(emitter, &array_len_symbol); emitter.instruction("str x0, [sp, #32]"); // save the supplied constructor argument count abi::emit_load_int_immediate(emitter, "x9", slot.params.len() as i64); @@ -1058,7 +1064,8 @@ fn emit_x86_64_validate_constructor_arg_count( fail_label: &str, ) { emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the eval argument array for arity validation - let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + let array_len_symbol = + abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_array_len"); abi::emit_call_label(emitter, &array_len_symbol); emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the supplied constructor argument count abi::emit_load_int_immediate(emitter, "r10", slot.params.len() as i64); @@ -1313,8 +1320,10 @@ fn emit_x86_64_constructor_ref_arg_cells( /// Loads one eval argument into an ARM64 spill slot as a boxed Mixed cell. fn emit_aarch64_load_eval_arg(module: &Module, emitter: &mut Emitter, index: usize) { - let value_int_symbol = module.target.extern_symbol("__elephc_eval_value_int"); - let array_get_symbol = module.target.extern_symbol("__elephc_eval_value_array_get"); + let value_int_symbol = + abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_int"); + let array_get_symbol = + abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_array_get"); abi::emit_load_int_immediate(emitter, "x0", index as i64); abi::emit_call_label(emitter, &value_int_symbol); emitter.instruction("str x0, [x29, #-16]"); // save the boxed index while loading from the argument array @@ -1326,8 +1335,10 @@ fn emit_aarch64_load_eval_arg(module: &Module, emitter: &mut Emitter, index: usi /// Loads one eval argument into an x86_64 spill slot as a boxed Mixed cell. fn emit_x86_64_load_eval_arg(module: &Module, emitter: &mut Emitter, index: usize) { - let value_int_symbol = module.target.extern_symbol("__elephc_eval_value_int"); - let array_get_symbol = module.target.extern_symbol("__elephc_eval_value_array_get"); + let value_int_symbol = + abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_int"); + let array_get_symbol = + abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_array_get"); abi::emit_load_int_immediate(emitter, "rdi", index as i64); abi::emit_call_label(emitter, &value_int_symbol); emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the boxed index while loading from the argument array @@ -1744,5 +1755,6 @@ fn class_id_for_scope(module: &Module, class_name: &str) -> u64 { /// Emits a platform-C global label for a user assembly helper. fn label_c_global(module: &Module, emitter: &mut Emitter, name: &str) { - emitter.label_global(&module.target.extern_symbol(name)); + debug_assert_eq!(module.target, emitter.target); + abi::emit_c_callback_entry(emitter, name); } diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index afa3140322..00af9fddfa 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -537,6 +537,8 @@ fn emit_static_method_call_x86_64( ) { let fail_label = "__elephc_eval_value_static_method_call_fail_x"; let done_label = "__elephc_eval_value_static_method_call_done_x"; + let scope_len_offset = abi::c_callback_stack_arg_offset(emitter.target, 6); + let context_offset = abi::c_callback_stack_arg_offset(emitter.target, 7); emitter.instruction("push rbp"); // preserve the Rust caller frame pointer emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer emitter.instruction(&format!("sub rsp, {}", STATIC_METHOD_HELPER_FRAME_SIZE)); // reserve aligned slots plus a boundary exception handler @@ -546,9 +548,9 @@ fn emit_static_method_call_x86_64( emitter.instruction("mov QWORD PTR [rbp - 32], r8"); // save the boxed eval argument array emitter.instruction("mov QWORD PTR [rbp - 48], rcx"); // save the requested method-name length emitter.instruction("mov QWORD PTR [rbp - 56], r9"); // save the active eval class-scope pointer - emitter.instruction("mov rax, QWORD PTR [rbp + 16]"); // load the active eval class-scope length stack argument + emitter.instruction(&format!("mov rax, QWORD PTR [rbp + {}]", scope_len_offset)); // load the active eval class-scope length stack argument emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // save the active eval class-scope length - emitter.instruction("mov rax, QWORD PTR [rbp + 24]"); // load the active eval context stack argument + emitter.instruction(&format!("mov rax, QWORD PTR [rbp + {}]", context_offset)); // load the active eval context stack argument emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // save the active eval context for callable descriptors emit_x86_64_static_method_dispatch(module, emitter, data, slots, fail_label); emitter.instruction(&format!("jmp {}", fail_label)); // no supported static method matched the request @@ -631,6 +633,7 @@ fn emit_method_call_x86_64( ) { let fail_label = "__elephc_eval_value_method_call_fail_x"; let done_label = "__elephc_eval_value_method_call_done_x"; + let context_offset = abi::c_callback_stack_arg_offset(emitter.target, 6); emitter.instruction("push rbp"); // preserve the Rust caller frame pointer emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer emitter.instruction(&format!("sub rsp, {}", METHOD_HELPER_FRAME_SIZE)); // reserve aligned slots plus a boundary exception handler @@ -639,7 +642,7 @@ fn emit_method_call_x86_64( emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the boxed eval argument array emitter.instruction("mov QWORD PTR [rbp - 48], r8"); // save the active eval class-scope pointer emitter.instruction("mov QWORD PTR [rbp - 56], r9"); // save the active eval class-scope length - emitter.instruction("mov rax, QWORD PTR [rbp + 16]"); // load the active eval context stack argument + emitter.instruction(&format!("mov rax, QWORD PTR [rbp + {}]", context_offset)); // load the active eval context stack argument emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // save the active eval context for callable descriptors emitter.instruction("test rdi, rdi"); // check whether the boxed receiver pointer is null emitter.instruction(&format!("jz {}", fail_label)); // null Mixed receiver cannot dispatch a method @@ -1224,7 +1227,8 @@ fn emit_aarch64_validate_builtin_throwable_method_arg_count( fail_label: &str, ) { emitter.instruction("ldr x0, [sp, #24]"); // reload the eval argument array for Throwable method arity validation - let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + let array_len_symbol = + abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_array_len"); abi::emit_call_label(emitter, &array_len_symbol); emitter.instruction("cmp x0, #0"); // compact Throwable methods accept no eval arguments emitter.instruction(&format!("b.ne {}", fail_label)); // reject unsupported Throwable method arguments from eval @@ -1237,7 +1241,8 @@ fn emit_x86_64_validate_builtin_throwable_method_arg_count( fail_label: &str, ) { emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the eval argument array for Throwable method arity validation - let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + let array_len_symbol = + abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_array_len"); abi::emit_call_label(emitter, &array_len_symbol); emitter.instruction("test rax, rax"); // compact Throwable methods accept no eval arguments emitter.instruction(&format!("jne {}", fail_label)); // reject unsupported Throwable method arguments from eval @@ -1331,6 +1336,9 @@ fn emit_x86_64_method_bodies( let receiver_ty = PhpType::Object(slot.class_name.clone()); let overflow_bytes = materialize_method_args(module, emitter, &receiver_ty, &slot.params, &slot.ref_params); + if slot.runtime_helper.is_some() { + abi::emit_windows_c_abi_registers_for_runtime_helper(emitter); + } let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); @@ -1499,7 +1507,8 @@ fn emit_aarch64_validate_method_arg_count( fail_label: &str, ) { emitter.instruction("ldr x0, [sp, #24]"); // reload the eval argument array for arity validation - let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + let array_len_symbol = + abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_array_len"); abi::emit_call_label(emitter, &array_len_symbol); abi::emit_load_int_immediate(emitter, "x9", slot.params.len() as i64); emitter.instruction("cmp x0, x9"); // compare supplied eval argument count with the method signature @@ -1514,7 +1523,8 @@ fn emit_x86_64_validate_method_arg_count( fail_label: &str, ) { emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the eval argument array for arity validation - let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + let array_len_symbol = + abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_array_len"); abi::emit_call_label(emitter, &array_len_symbol); abi::emit_load_int_immediate(emitter, "r10", slot.params.len() as i64); emitter.instruction("cmp rax, r10"); // compare supplied eval argument count with the method signature @@ -1529,7 +1539,8 @@ fn emit_aarch64_validate_static_method_arg_count( fail_label: &str, ) { emitter.instruction("ldr x0, [sp, #24]"); // reload the eval argument array for static arity validation - let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + let array_len_symbol = + abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_array_len"); abi::emit_call_label(emitter, &array_len_symbol); abi::emit_load_int_immediate(emitter, "x9", slot.params.len() as i64); emitter.instruction("cmp x0, x9"); // compare supplied eval argument count with the static method signature @@ -1544,7 +1555,8 @@ fn emit_x86_64_validate_static_method_arg_count( fail_label: &str, ) { emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the eval argument array for static arity validation - let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + let array_len_symbol = + abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_array_len"); abi::emit_call_label(emitter, &array_len_symbol); abi::emit_load_int_immediate(emitter, "r10", slot.params.len() as i64); emitter.instruction("cmp rax, r10"); // compare supplied eval argument count with the static method signature @@ -1903,8 +1915,10 @@ fn emit_aarch64_load_eval_arg( index: usize, arg_array_frame_offset: usize, ) { - let value_int_symbol = module.target.extern_symbol("__elephc_eval_value_int"); - let array_get_symbol = module.target.extern_symbol("__elephc_eval_value_array_get"); + let value_int_symbol = + abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_int"); + let array_get_symbol = + abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_array_get"); abi::emit_load_int_immediate(emitter, "x0", index as i64); abi::emit_call_label(emitter, &value_int_symbol); emitter.instruction("str x0, [x29, #-16]"); // save the boxed index while loading from the argument array @@ -1916,8 +1930,10 @@ fn emit_aarch64_load_eval_arg( /// Loads one eval argument into an x86_64 spill slot as a boxed Mixed cell. fn emit_x86_64_load_eval_arg(module: &Module, emitter: &mut Emitter, index: usize) { - let value_int_symbol = module.target.extern_symbol("__elephc_eval_value_int"); - let array_get_symbol = module.target.extern_symbol("__elephc_eval_value_array_get"); + let value_int_symbol = + abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_int"); + let array_get_symbol = + abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_array_get"); abi::emit_load_int_immediate(emitter, "rdi", index as i64); abi::emit_call_label(emitter, &value_int_symbol); emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the boxed index while loading from the argument array @@ -2014,7 +2030,7 @@ fn emit_aarch64_cast_eval_object_arg( fail_label: &str, ) { let (label, len) = data.add_string(class_name.as_bytes()); - let is_a_symbol = module.target.extern_symbol("__elephc_eval_value_is_a"); + let is_a_symbol = abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_is_a"); emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for object type validation abi::emit_symbol_address(emitter, "x1", &label); abi::emit_load_int_immediate(emitter, "x2", len as i64); @@ -2180,7 +2196,7 @@ fn emit_x86_64_cast_eval_object_arg( fail_label: &str, ) { let (label, len) = data.add_string(class_name.as_bytes()); - let is_a_symbol = module.target.extern_symbol("__elephc_eval_value_is_a"); + let is_a_symbol = abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_is_a"); emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for object type validation abi::emit_symbol_address(emitter, "rsi", &label); abi::emit_load_int_immediate(emitter, "rdx", len as i64); @@ -2410,6 +2426,6 @@ fn label_fragment(value: &str) -> String { /// Emits a C-visible global label with target-specific symbol mangling. fn label_c_global(module: &Module, emitter: &mut Emitter, name: &str) { - let symbol = module.target.extern_symbol(name); - emitter.label_global(&symbol); + debug_assert_eq!(module.target, emitter.target); + abi::emit_c_callback_entry(emitter, name); } diff --git a/src/codegen/eval_property_helpers.rs b/src/codegen/eval_property_helpers.rs index 2bbcecfdfe..2ef6bd3851 100644 --- a/src/codegen/eval_property_helpers.rs +++ b/src/codegen/eval_property_helpers.rs @@ -1248,7 +1248,8 @@ fn emit_aarch64_store_object_property_slot( ) { if !class_name.is_empty() { let (label, len) = data.add_string(class_name.as_bytes()); - let is_a_symbol = module.target.extern_symbol("__elephc_eval_value_is_a"); + let is_a_symbol = + abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_is_a"); emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value for object type validation abi::emit_symbol_address(emitter, "x1", &label); abi::emit_load_int_immediate(emitter, "x2", len as i64); @@ -1289,7 +1290,8 @@ fn emit_x86_64_store_object_property_slot( ) { if !class_name.is_empty() { let (label, len) = data.add_string(class_name.as_bytes()); - let is_a_symbol = module.target.extern_symbol("__elephc_eval_value_is_a"); + let is_a_symbol = + abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_is_a"); emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the boxed eval value for object type validation abi::emit_symbol_address(emitter, "rsi", &label); abi::emit_load_int_immediate(emitter, "rdx", len as i64); @@ -1435,6 +1437,6 @@ fn label_fragment(value: &str) -> String { /// Emits a C-visible global label with target-specific symbol mangling. fn label_c_global(module: &Module, emitter: &mut Emitter, name: &str) { - let symbol = module.target.extern_symbol(name); - emitter.label_global(&symbol); + debug_assert_eq!(module.target, emitter.target); + abi::emit_c_callback_entry(emitter, name); } diff --git a/src/codegen/eval_reflection_helpers.rs b/src/codegen/eval_reflection_helpers.rs index de7555505e..3f2cb1e8ba 100644 --- a/src/codegen/eval_reflection_helpers.rs +++ b/src/codegen/eval_reflection_helpers.rs @@ -352,6 +352,6 @@ fn emit_set_repeated_property_x86_64(emitter: &mut Emitter, layout: &ReflectionA /// Emits a C-visible global label with target-specific symbol mangling. fn label_c_global(module: &Module, emitter: &mut Emitter, name: &str) { - let symbol = module.target.extern_symbol(name); - emitter.label_global(&symbol); + debug_assert_eq!(module.target, emitter.target); + abi::emit_c_callback_entry(emitter, name); } diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index db0760196c..bb813cd8ab 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -653,6 +653,10 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO let named_type_label = "__elephc_eval_reflection_owner_new_named_type_x"; let union_type_label = "__elephc_eval_reflection_owner_new_union_type_x"; let intersection_type_label = "__elephc_eval_reflection_owner_new_intersection_type_x"; + let target = emitter.target; + let stack_arg_offset = |argument_index| { + abi::c_callback_stack_arg_offset(target, argument_index) + }; emitter.instruction("push rbp"); // preserve the Rust caller frame pointer emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer emitter.instruction("sub rsp, 144"); // reserve slots for inputs, object, metadata arrays, and name parts @@ -662,25 +666,25 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the boxed ReflectionAttribute array emitter.instruction("mov QWORD PTR [rbp - 88], r8"); // save the boxed ReflectionClass interface-name array emitter.instruction("mov QWORD PTR [rbp - 96], r9"); // save the boxed ReflectionClass trait-name array - emitter.instruction("mov rax, QWORD PTR [rbp + 16]"); // load the boxed ReflectionClass method-name array from the first stack argument + emitter.instruction(&format!("mov rax, QWORD PTR [rbp + {}]", stack_arg_offset(6))); // load the boxed ReflectionClass method-name array from the first stack argument emitter.instruction("mov QWORD PTR [rbp - 112], rax"); // save the boxed ReflectionClass method-name array - emitter.instruction("mov rax, QWORD PTR [rbp + 24]"); // load the boxed ReflectionClass property-name array from the second stack argument + emitter.instruction(&format!("mov rax, QWORD PTR [rbp + {}]", stack_arg_offset(7))); // load the boxed ReflectionClass property-name array from the second stack argument emitter.instruction("mov QWORD PTR [rbp - 120], rax"); // save the boxed ReflectionClass property-name array - emitter.instruction("mov rax, QWORD PTR [rbp + 32]"); // load the boxed ReflectionClass method objects array from the third stack argument + emitter.instruction(&format!("mov rax, QWORD PTR [rbp + {}]", stack_arg_offset(8))); // load the boxed ReflectionClass method objects array from the third stack argument emitter.instruction("mov QWORD PTR [rbp - 128], rax"); // save the boxed ReflectionClass method objects array - emitter.instruction("mov rax, QWORD PTR [rbp + 40]"); // load the boxed ReflectionClass property objects array from the fourth stack argument + emitter.instruction(&format!("mov rax, QWORD PTR [rbp + {}]", stack_arg_offset(9))); // load the boxed ReflectionClass property objects array from the fourth stack argument emitter.instruction("mov QWORD PTR [rbp - 136], rax"); // save the boxed ReflectionClass property objects array - emitter.instruction("mov rax, QWORD PTR [rbp + 48]"); // load the boxed ReflectionClass parent value from the fifth stack argument + emitter.instruction(&format!("mov rax, QWORD PTR [rbp + {}]", stack_arg_offset(10))); // load the boxed ReflectionClass parent value from the fifth stack argument emitter.instruction("mov QWORD PTR [rbp - 144], rax"); // save the boxed ReflectionClass parent value - emitter.instruction("mov rax, QWORD PTR [rbp + 56]"); // load ReflectionClass modifier flags from the sixth stack argument + emitter.instruction(&format!("mov rax, QWORD PTR [rbp + {}]", stack_arg_offset(11))); // load ReflectionClass modifier flags from the sixth stack argument emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // save ReflectionClass modifier flags - emitter.instruction("mov rax, QWORD PTR [rbp + 64]"); // load owner modifier/count metadata from the seventh stack argument + emitter.instruction(&format!("mov rax, QWORD PTR [rbp + {}]", stack_arg_offset(12))); // load owner modifier/count metadata from the seventh stack argument emitter.instruction("mov QWORD PTR [rbp - 104], rax"); // save owner modifier/count metadata - emitter.instruction("mov rax, QWORD PTR [rbp + 72]"); // load ReflectionMethod getModifiers bitmask from the eighth stack argument + emitter.instruction(&format!("mov rax, QWORD PTR [rbp + {}]", stack_arg_offset(13))); // load ReflectionMethod getModifiers bitmask from the eighth stack argument emitter.instruction("mov QWORD PTR [rbp - 80], rax"); // save ReflectionMethod getModifiers bitmask - emitter.instruction("mov rax, QWORD PTR [rbp + 80]"); // load boxed ReflectionClassConstant value from the ninth stack argument + emitter.instruction(&format!("mov rax, QWORD PTR [rbp + {}]", stack_arg_offset(14))); // load boxed ReflectionClassConstant value from the ninth stack argument emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // save boxed ReflectionClassConstant value - emitter.instruction("mov rax, QWORD PTR [rbp + 88]"); // load boxed ReflectionEnumBackedCase backing value from the tenth stack argument + emitter.instruction(&format!("mov rax, QWORD PTR [rbp + {}]", stack_arg_offset(15))); // load boxed ReflectionEnumBackedCase backing value from the tenth stack argument emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // save boxed ReflectionEnumBackedCase backing value emitter.instruction("cmp rdi, 0"); // owner kind 0 means ReflectionClass emitter.instruction(&format!("je {}", class_label)); // allocate a ReflectionClass owner @@ -2304,7 +2308,8 @@ fn emit_set_owner_constructor_property_x86_64( let (Some(low), Some(high)) = (layout.constructor_lo, layout.constructor_hi) else { return; }; - emitter.instruction("mov rax, QWORD PTR [rbp + 96]"); // reload the boxed ReflectionClass constructor value + let constructor_offset = abi::c_callback_stack_arg_offset(emitter.target, 16); + emitter.instruction(&format!("mov rax, QWORD PTR [rbp + {}]", constructor_offset)); // reload the boxed ReflectionClass constructor value emitter.instruction("test rax, rax"); // check whether the boxed constructor value is null emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null constructor metadata emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the boxed constructor value across incref @@ -2559,6 +2564,6 @@ fn emit_set_owner_attrs_property_x86_64( /// Emits a C-visible global label with target-specific symbol mangling. fn label_c_global(module: &Module, emitter: &mut Emitter, name: &str) { - let symbol = module.target.extern_symbol(name); - emitter.label_global(&symbol); + debug_assert_eq!(module.target, emitter.target); + abi::emit_c_callback_entry(emitter, name); } diff --git a/src/codegen/eval_static_property_helpers.rs b/src/codegen/eval_static_property_helpers.rs index 8073517d82..1712798ecb 100644 --- a/src/codegen/eval_static_property_helpers.rs +++ b/src/codegen/eval_static_property_helpers.rs @@ -362,6 +362,7 @@ fn emit_static_property_set_x86_64( ) { let fail_label = "__elephc_eval_value_static_property_set_fail_x"; let done_label = "__elephc_eval_value_static_property_set_done_x"; + let scope_len_offset = abi::c_callback_stack_arg_offset(emitter.target, 6); emitter.instruction("push rbp"); // preserve the Rust caller frame pointer emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer emitter.instruction("sub rsp, 64"); // reserve aligned slots for class, property, value, and scope @@ -371,7 +372,7 @@ fn emit_static_property_set_x86_64( emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested property-name length emitter.instruction("mov QWORD PTR [rbp - 40], r8"); // save the boxed value being assigned emitter.instruction("mov QWORD PTR [rbp - 48], r9"); // save the active eval class-scope pointer - emitter.instruction("mov rax, QWORD PTR [rbp + 16]"); // load the active eval class-scope length stack argument + emitter.instruction(&format!("mov rax, QWORD PTR [rbp + {}]", scope_len_offset)); // load the active eval class-scope length stack argument emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // save the active eval class-scope length emit_x86_64_static_property_dispatch(module, emitter, data, slots, "set"); emitter.instruction(&format!("jmp {}", fail_label)); // no supported static property matched the request @@ -996,7 +997,8 @@ fn emit_aarch64_store_object_static_property_slot( ) { if !class_name.is_empty() { let (label, len) = data.add_string(class_name.as_bytes()); - let is_a_symbol = module.target.extern_symbol("__elephc_eval_value_is_a"); + let is_a_symbol = + abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_is_a"); emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed eval value for object type validation abi::emit_symbol_address(emitter, "x1", &label); abi::emit_load_int_immediate(emitter, "x2", len as i64); @@ -1036,7 +1038,8 @@ fn emit_x86_64_store_object_static_property_slot( ) { if !class_name.is_empty() { let (label, len) = data.add_string(class_name.as_bytes()); - let is_a_symbol = module.target.extern_symbol("__elephc_eval_value_is_a"); + let is_a_symbol = + abi::c_callback_internal_symbol(module.target, "__elephc_eval_value_is_a"); emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload the boxed eval value for object type validation abi::emit_symbol_address(emitter, "rsi", &label); abi::emit_load_int_immediate(emitter, "rdx", len as i64); @@ -1221,6 +1224,6 @@ fn label_fragment(value: &str) -> String { /// Emits a C-visible global label with target-specific symbol mangling. fn label_c_global(module: &Module, emitter: &mut Emitter, name: &str) { - let symbol = module.target.extern_symbol(name); - emitter.label_global(&symbol); + debug_assert_eq!(module.target, emitter.target); + abi::emit_c_callback_entry(emitter, name); } diff --git a/src/codegen/frame.rs b/src/codegen/frame.rs index 2c93c38120..25e847d903 100644 --- a/src/codegen/frame.rs +++ b/src/codegen/frame.rs @@ -15,7 +15,7 @@ use std::collections::{HashMap, HashSet}; use crate::codegen::abi; use crate::codegen::emit::Emitter; -use crate::codegen::platform::{Arch, Target}; +use crate::codegen::platform::{Arch, Platform, Target}; use crate::codegen::{ emit_box_current_value_as_mixed, emit_write_current_string_stderr, emit_write_literal_stderr, }; @@ -161,6 +161,33 @@ fn try_handler_tokens(function: &Function) -> Vec { } /// Emits the process-entry prologue for the EIR main function. +/// Emits `signal(SIGPIPE, SIG_IGN)` at program start on the POSIX targets. +/// +/// PHP does the same (`php_ignore_sigpipe`, and the CLI SAPI sets the disposition +/// before running any script), so writing to a socket or pipe whose peer has gone +/// away makes `fwrite()` report failure instead of killing the process. Without +/// it a compiled program dies from signal 13 with no output at all — the whole +/// script is lost, not just the write. Windows has no SIGPIPE: a broken pipe there +/// surfaces as an ordinary `WriteFile` error, which the shims already translate. +fn emit_ignore_sigpipe(ctx: &mut FunctionContext<'_>) { + if ctx.emitter.target.platform == Platform::Windows { + return; + } + ctx.emitter.comment("ignore SIGPIPE so a dead peer fails the write, not the process"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x0, #13"); // SIGPIPE + ctx.emitter.instruction("mov x1, #1"); // SIG_IGN + ctx.emitter.bl_c("signal"); // install the ignore disposition + } + Arch::X86_64 => { + ctx.emitter.instruction("mov edi, 13"); // SIGPIPE + ctx.emitter.instruction("mov esi, 1"); // SIG_IGN + ctx.emitter.emit_call_c("signal"); // install the ignore disposition + } + } +} + pub(super) fn emit_main_prologue(ctx: &mut FunctionContext<'_>) { if ctx.emitter.target.arch == Arch::AArch64 { ctx.emitter.raw(".align 2"); @@ -172,6 +199,7 @@ pub(super) fn emit_main_prologue(ctx: &mut FunctionContext<'_>) { emit_callee_saved_saves(ctx); ctx.emitter.comment("save argc/argv to globals"); abi::emit_store_process_args_to_globals(ctx.emitter); + emit_ignore_sigpipe(ctx); if ctx.heap_debug { ctx.emitter.comment("enable heap debug flag"); abi::emit_enable_heap_debug_flag(ctx.emitter); @@ -280,6 +308,7 @@ pub(super) fn emit_main_epilogue(ctx: &mut FunctionContext<'_>) { emit_main_local_epilogue_cleanup(ctx); emit_main_static_local_cleanup(ctx); emit_main_global_epilogue_cleanup(ctx); + emit_process_bootstrap_cleanup(ctx); emit_callee_saved_restores(ctx); abi::emit_frame_restore(ctx.emitter, ctx.frame_size); if ctx.gc_stats { @@ -294,6 +323,17 @@ pub(super) fn emit_main_epilogue(ctx: &mut FunctionContext<'_>) { ctx.epilogue_emitted = true; } +/// Releases process-bootstrap storage that participates in runtime heap +/// accounting before exit diagnostics inspect the allocator. +fn emit_process_bootstrap_cleanup(ctx: &mut FunctionContext<'_>) { + if (ctx.emitter.platform, ctx.emitter.target.arch) != (Platform::Windows, Arch::X86_64) { + return; + } + ctx.emitter + .comment("release Windows UTF-8 argv bootstrap storage"); + abi::emit_call_label(ctx.emitter, "__rt_sys_free_argv"); +} + /// 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(); @@ -381,6 +421,7 @@ pub(super) fn emit_web_handler_prologue(ctx: &mut FunctionContext<'_>) { } ctx.emitter.blank(); ctx.emitter.label_global(WEB_HANDLER_SYMBOL); + emit_web_handler_abi_saves(ctx); 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 @@ -417,10 +458,89 @@ pub(super) fn emit_web_handler_epilogue(ctx: &mut FunctionContext<'_>) { } emit_callee_saved_restores(ctx); abi::emit_frame_restore(ctx.emitter, ctx.frame_size); + emit_web_handler_abi_restores(ctx); abi::emit_return(ctx.emitter); ctx.epilogue_emitted = true; } +/// Preserves the ABI callee-saved registers that elephc's internal convention treats as +/// scratch, at the one place in a `--web` program that is a real ABI boundary. +/// +/// `_elephc_web_handler` is an `extern "C" fn()` invoked directly by the bridge's Rust +/// server loop, so it owes that caller the platform's callee-saved contract. The register +/// allocator does not discharge it: `emit_callee_saved_saves` only spills +/// `used_callee_saved()`, drawn from pools that deliberately exclude these registers -- +/// AArch64 allocates x21-x28, x86_64 allocates only rbx. +/// +/// What the pools leave out is used anyway. `abi::nested_call_reg` hands out x19 and r12 +/// to preserve frame pointers across nested calls; the x86_64 pool's own comment records +/// that r14/r15 are used as scratch by hand-written runtime routines "without +/// ABI-compliant save/restore"; and `abi::float_spill_scratch_reg` stages float spills in +/// xmm15. On Windows the asymmetry is wider still, because elephc's internal convention is +/// SysV: rdi and rsi are argument scratch there and callee-saved under MSx64, and +/// xmm6-xmm15 are callee-saved rather than the volatile range SysV makes them (helpers +/// such as `__rt_php_round` write xmm6/xmm7 freely). +/// +/// The saves precede `emit_frame_prologue` deliberately: AArch64 addresses locals relative +/// to `sp`, so pushing after the frame is established would shift every local's offset. +/// Each block is a multiple of 16 bytes, so stack alignment at entry is preserved. +fn emit_web_handler_abi_saves(ctx: &mut FunctionContext<'_>) { + ctx.emitter + .comment("preserve ABI callee-saved registers the allocator pools exclude"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("stp x19, x20, [sp, #-16]!"); // x19 backs nested_call_reg; neither is in the allocator pool + } + Arch::X86_64 => { + for reg in ["r12", "r13", "r14", "r15"] { + ctx.emitter.instruction(&format!("push {}", reg)); // r12 backs nested_call_reg; r14/r15 are runtime scratch + } + if ctx.emitter.target.platform == Platform::Windows { + for reg in ["rdi", "rsi"] { + ctx.emitter.instruction(&format!("push {}", reg)); // SysV argument scratch, but callee-saved under MSx64 + } + ctx.emitter.instruction("sub rsp, 160"); // room for the ten MSx64 callee-saved xmm registers + for idx in 0..10 { + ctx.emitter.instruction(&format!( + "movups XMMWORD PTR [rsp + {}], xmm{}", + idx * 16, + idx + 6 + )); // xmm6-xmm15 are volatile under SysV and preserved under MSx64 + } + } + } + } +} + +/// Restores what [`emit_web_handler_abi_saves`] preserved, in the mirror order. +fn emit_web_handler_abi_restores(ctx: &mut FunctionContext<'_>) { + ctx.emitter + .comment("restore ABI callee-saved registers before returning to the bridge"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("ldp x19, x20, [sp], #16"); // release the pair saved ahead of the frame + } + Arch::X86_64 => { + if ctx.emitter.target.platform == Platform::Windows { + for idx in 0..10 { + ctx.emitter.instruction(&format!( + "movups xmm{}, XMMWORD PTR [rsp + {}]", + idx + 6, + idx * 16 + )); // reload the MSx64 callee-saved xmm range + } + ctx.emitter.instruction("add rsp, 160"); // release the xmm save area + for reg in ["rsi", "rdi"] { + ctx.emitter.instruction(&format!("pop {}", reg)); // mirror order of the MSx64-only integer saves + } + } + for reg in ["r15", "r14", "r13", "r12"] { + ctx.emitter.instruction(&format!("pop {}", reg)); // mirror order of the shared integer saves + } + } + } +} + /// 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 @@ -452,7 +572,14 @@ pub(super) fn emit_web_entry_stub(ctx: &mut FunctionContext<'_>) { // 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"); + let call_pad = abi::outgoing_call_stack_pad_bytes(target, 0); + if call_pad > 0 { + abi::emit_reserve_temporary_stack(ctx.emitter, call_pad); + } abi::emit_call_label(ctx.emitter, &bridge_entry); + if call_pad > 0 { + abi::emit_release_temporary_stack(ctx.emitter, call_pad); + } abi::emit_exit_with_result_reg(ctx.emitter); } diff --git a/src/codegen/frame/tests.rs b/src/codegen/frame/tests.rs index 4b42beda19..6deedcb31f 100644 --- a/src/codegen/frame/tests.rs +++ b/src/codegen/frame/tests.rs @@ -10,6 +10,8 @@ use super::*; use crate::codegen::generate_user_asm_from_ir; use crate::codegen::platform::{Arch, Platform, Target}; +use crate::codegen::shared_state::SharedCodegenState; +use crate::codegen_support::data_section::DataSection; use crate::ir::{Builder, FunctionParam, IrType, Module, Terminator}; /// Verifies AArch64 saves a later Mixed argument before retaining an earlier string. @@ -128,3 +130,98 @@ fn owned_string_then_mixed_prologue_asm(target: Target) -> String { generate_user_asm_from_ir(&module, false, false) .expect("parameter-prologue fixture should lower") } + +/// Verifies Windows releases UTF-8 argv while the aligned main frame is still active. +#[test] +fn windows_main_argv_cleanup_precedes_frame_restore() { + let asm = empty_main_asm(Target::new(Platform::Windows, Arch::X86_64)); + let cleanup = asm + .find("call __rt_sys_free_argv") + .expect("Windows main must release its owned UTF-8 argv storage"); + let restore = asm[cleanup..] + .find("pop rbp") + .map(|offset| cleanup + offset) + .expect("Windows main must restore its frame after argv cleanup"); + + assert!(cleanup < restore, "argv cleanup ran after frame restore:\n{asm}"); +} + +/// Verifies non-Windows main epilogues do not acquire the Windows argv cleanup call. +#[test] +fn non_windows_main_epilogues_omit_argv_cleanup() { + for target in [ + Target::new(Platform::Linux, Arch::X86_64), + Target::new(Platform::Linux, Arch::AArch64), + ] { + let asm = empty_main_asm(target); + assert!(!asm.contains("__rt_sys_free_argv"), "{target:?}:\n{asm}"); + } +} + +/// Builds an empty top-level function for target-specific epilogue assertions. +fn empty_main_asm(target: Target) -> String { + let mut module = Module::new(target); + let mut main = Function::new("main".to_string(), IrType::Void, PhpType::Void); + main.flags.is_main = true; + { + let mut builder = Builder::new(&mut main); + let entry = builder.create_named_block("entry", Vec::new()); + builder.set_entry(entry); + builder.position_at_end(entry); + builder.terminate(Terminator::Return { value: None }); + } + module.add_function(main); + + generate_user_asm_from_ir(&module, false, false).expect("empty main fixture should lower") +} + +/// Verifies the Windows web process-entry stub reserves the mandatory MSx64 +/// shadow area around its direct platform-ABI call into `elephc_web_run`. +#[test] +fn windows_web_entry_stub_reserves_native_shadow_space() { + let asm = web_entry_stub_asm(Target::new(Platform::Windows, Arch::X86_64)); + let reserve = asm.find("sub rsp, 32").expect("Windows shadow-space reserve"); + let call = asm + .find("\n call elephc_web_run\n") + .expect("web bridge entry call"); + let release = asm.find("add rsp, 32").expect("Windows shadow-space release"); + assert!(reserve < call && call < release, "{asm}"); + assert!(asm.contains("mov rcx"), "argc must use the first MSx64 register: {asm}"); + assert!(asm.contains("mov rdx"), "argv must use the second MSx64 register: {asm}"); +} + +/// Verifies Linux x86 retains the direct web-entry call without Windows shadow space. +#[test] +fn linux_web_entry_stub_remains_direct() { + let asm = web_entry_stub_asm(Target::new(Platform::Linux, Arch::X86_64)); + assert!(asm.contains("call elephc_web_run")); + assert!(!asm.contains("sub rsp, 32")); + assert!(!asm.contains("add rsp, 32")); +} + +/// Renders only the web process-entry stub for ABI-structure assertions. +fn web_entry_stub_asm(target: Target) -> String { + let module = Module::new(target); + let mut function = Function::new("main".to_string(), IrType::Void, PhpType::Void); + function.flags.is_main = true; + let layout = layout_for_function(&function, target, false); + let mut emitter = Emitter::new(target); + let mut data = DataSection::new(); + let mut shared = SharedCodegenState::default(); + { + let mut ctx = FunctionContext::new( + &module, + &function, + &mut emitter, + &mut data, + &mut shared, + layout, + true, + false, + false, + None, + ); + emit_web_entry_stub(&mut ctx); + } + emitter.output() +} diff --git a/src/codegen/literal_defaults.rs b/src/codegen/literal_defaults.rs index cbcfe18de4..4fe1431229 100644 --- a/src/codegen/literal_defaults.rs +++ b/src/codegen/literal_defaults.rs @@ -254,8 +254,8 @@ pub(crate) fn emit_empty_assoc_array_literal_to_result( ctx: &mut FunctionContext<'_>, value_type: &PhpType, ) { - let capacity_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); - let value_tag_reg = abi::int_arg_reg_name(ctx.emitter.target, 1); + let capacity_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); + let value_tag_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 1); abi::emit_load_int_immediate(ctx.emitter, capacity_reg, 16); abi::emit_load_int_immediate( ctx.emitter, diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index e1f1b8c336..d99e2f983c 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -9,7 +9,7 @@ //! - Results are written to fixed value-placement slots immediately after definition. //! - Unsupported opcodes fail explicitly instead of silently emitting invalid code. -use crate::codegen::platform::Arch; +use crate::codegen::platform::{Arch, Platform}; use crate::codegen::{ abi, callable_descriptor, callable_invoker_args, emit_box_current_owned_value_as_mixed, emit_box_current_value_as_mixed, emit_box_runtime_payload_as_mixed, runtime, @@ -1094,8 +1094,11 @@ fn emit_instance_method_descriptor_entry_wrapper_body( let actual_assignments = abi::build_outgoing_arg_assignments_for_target(ctx.emitter.target, &actual_types, 0); let (incoming_stack_offsets, _) = descriptor_entry_stack_offsets(&incoming_assignments); - let (actual_stack_offsets, actual_overflow_bytes) = + let (mut actual_stack_offsets, actual_overflow_bytes) = descriptor_entry_stack_offsets(&actual_assignments); + let actual_call_pad_bytes = abi::outgoing_call_stack_pad_bytes(ctx.emitter.target, 0); + shift_descriptor_entry_stack_offsets(&mut actual_stack_offsets, actual_call_pad_bytes); + let actual_call_stack_bytes = actual_overflow_bytes + actual_call_pad_bytes; let frame_size = descriptor_entry_frame_size(incoming_types.len()); abi::emit_frame_prologue(ctx.emitter, frame_size); @@ -1112,8 +1115,8 @@ fn emit_instance_method_descriptor_entry_wrapper_body( incoming_stack_offsets[idx], ); } - if actual_overflow_bytes > 0 { - abi::emit_reserve_temporary_stack(ctx.emitter, actual_overflow_bytes); + if actual_call_stack_bytes > 0 { + abi::emit_reserve_temporary_stack(ctx.emitter, actual_call_stack_bytes); } for (idx, (ty, assignment)) in actual_types .iter() @@ -1134,8 +1137,8 @@ fn emit_instance_method_descriptor_entry_wrapper_body( ); } abi::emit_call_label(ctx.emitter, &method_symbol(class_name, method_key)); - if actual_overflow_bytes > 0 { - abi::emit_release_temporary_stack(ctx.emitter, actual_overflow_bytes); + if actual_call_stack_bytes > 0 { + abi::emit_release_temporary_stack(ctx.emitter, actual_call_stack_bytes); } abi::emit_frame_restore(ctx.emitter, frame_size); abi::emit_return(ctx.emitter); @@ -1160,8 +1163,11 @@ fn emit_static_method_descriptor_entry_wrapper_body( let actual_assignments = abi::build_outgoing_arg_assignments_for_target(ctx.emitter.target, &actual_types, 0); let (incoming_stack_offsets, _) = descriptor_entry_stack_offsets(&incoming_assignments); - let (actual_stack_offsets, actual_overflow_bytes) = + let (mut actual_stack_offsets, actual_overflow_bytes) = descriptor_entry_stack_offsets(&actual_assignments); + let actual_call_pad_bytes = abi::outgoing_call_stack_pad_bytes(ctx.emitter.target, 0); + shift_descriptor_entry_stack_offsets(&mut actual_stack_offsets, actual_call_pad_bytes); + let actual_call_stack_bytes = actual_overflow_bytes + actual_call_pad_bytes; let frame_size = descriptor_entry_frame_size(visible_arg_types.len()); abi::emit_frame_prologue(ctx.emitter, frame_size); @@ -1178,8 +1184,8 @@ fn emit_static_method_descriptor_entry_wrapper_body( incoming_stack_offsets[idx], ); } - if actual_overflow_bytes > 0 { - abi::emit_reserve_temporary_stack(ctx.emitter, actual_overflow_bytes); + if actual_call_stack_bytes > 0 { + abi::emit_reserve_temporary_stack(ctx.emitter, actual_call_stack_bytes); } for (idx, (ty, assignment)) in actual_types .iter() @@ -1204,8 +1210,8 @@ fn emit_static_method_descriptor_entry_wrapper_body( } } abi::emit_call_label(ctx.emitter, &static_method_symbol(impl_class, method_key)); - if actual_overflow_bytes > 0 { - abi::emit_release_temporary_stack(ctx.emitter, actual_overflow_bytes); + if actual_call_stack_bytes > 0 { + abi::emit_release_temporary_stack(ctx.emitter, actual_call_stack_bytes); } abi::emit_frame_restore(ctx.emitter, frame_size); abi::emit_return(ctx.emitter); @@ -1227,8 +1233,11 @@ fn emit_static_late_bound_descriptor_entry_wrapper_body( let actual_assignments = abi::build_outgoing_arg_assignments_for_target(ctx.emitter.target, &actual_types, 0); let (incoming_stack_offsets, _) = descriptor_entry_stack_offsets(&incoming_assignments); - let (actual_stack_offsets, actual_overflow_bytes) = + let (mut actual_stack_offsets, actual_overflow_bytes) = descriptor_entry_stack_offsets(&actual_assignments); + let actual_call_pad_bytes = abi::outgoing_call_stack_pad_bytes(ctx.emitter.target, 0); + shift_descriptor_entry_stack_offsets(&mut actual_stack_offsets, actual_call_pad_bytes); + let actual_call_stack_bytes = actual_overflow_bytes + actual_call_pad_bytes; let frame_size = descriptor_entry_frame_size(incoming_types.len()); abi::emit_frame_prologue(ctx.emitter, frame_size); @@ -1245,8 +1254,8 @@ fn emit_static_late_bound_descriptor_entry_wrapper_body( incoming_stack_offsets[idx], ); } - if actual_overflow_bytes > 0 { - abi::emit_reserve_temporary_stack(ctx.emitter, actual_overflow_bytes); + if actual_call_stack_bytes > 0 { + abi::emit_reserve_temporary_stack(ctx.emitter, actual_call_stack_bytes); } for (idx, (ty, assignment)) in actual_types .iter() @@ -1271,8 +1280,8 @@ fn emit_static_late_bound_descriptor_entry_wrapper_body( } else { abi::emit_call_label(ctx.emitter, &static_method_symbol(impl_class, method_key)); } - if actual_overflow_bytes > 0 { - abi::emit_release_temporary_stack(ctx.emitter, actual_overflow_bytes); + if actual_call_stack_bytes > 0 { + abi::emit_release_temporary_stack(ctx.emitter, actual_call_stack_bytes); } abi::emit_frame_restore(ctx.emitter, frame_size); abi::emit_return(ctx.emitter); @@ -1356,6 +1365,13 @@ fn descriptor_entry_stack_offsets( (offsets, next_offset) } +/// Shifts descriptor overflow arguments above the platform caller-stack prefix. +fn shift_descriptor_entry_stack_offsets(offsets: &mut [Option], prefix_bytes: usize) { + for offset in offsets.iter_mut().flatten() { + *offset += prefix_bytes; + } +} + /// Lowers an explicit cycle-collection safe point. fn lower_gc_collect(ctx: &mut FunctionContext<'_>) -> Result<()> { abi::emit_call_label(ctx.emitter, "__rt_gc_collect_cycles"); @@ -1967,6 +1983,12 @@ fn first_class_builtin_descriptor( target: &str, ) -> Result> { let name = php_symbol_key(target.trim_start_matches('\\')); + if !crate::types::checker::builtins::builtin_available_on_platform( + &name, + ctx.emitter.target.platform, + ) { + return Ok(None); + } let Some(sig) = first_class_callable_builtin_sig(&name) else { return Ok(None); }; @@ -3014,13 +3036,26 @@ fn lower_try_push_handler(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> ); abi::emit_frame_slot_address(ctx.emitter, scratch, handler_offset); abi::emit_store_reg_to_symbol(ctx.emitter, scratch, "_exc_handler_top", 0); + // setjmp is a runtime-helper-convention call: on Windows it routes to the + // SEH-free __rt_setjmp (SysV rdi), and on Linux the C setjmp also takes its + // jmp_buf in the SysV arg-0 register, so stage the address in the SysV-fixed + // runtime-helper register (rdi/x0) on every target rather than the MSx64 arg + // register (rcx) `int_arg_reg_name` would pick on Windows. abi::emit_frame_slot_address( ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 0), + abi::runtime_helper_int_arg_reg(ctx.emitter, 0), handler_offset - TRY_HANDLER_JMP_BUF_OFFSET, ); ctx.emitter.bl_c("setjmp"); - abi::emit_branch_if_int_result_nonzero(ctx.emitter, &handler_label); + match ctx.emitter.target.arch { + Arch::AArch64 => { + let resume_label = ctx.next_label("try_handler_resume"); + ctx.emitter.instruction(&format!("cbz x0, {}", resume_label)); // keep the normal setjmp path within the AArch64 conditional-branch range + ctx.emitter.instruction(&format!("b {}", handler_label)); // take a long-range handler edge after longjmp resumes the frame + ctx.emitter.label(&resume_label); + } + Arch::X86_64 => abi::emit_branch_if_int_result_nonzero(ctx.emitter, &handler_label), + } Ok(()) } @@ -3674,6 +3709,11 @@ fn lower_instance_runtime_intrinsic( materialize_direct_call_args_with_refs(ctx, &inst.operands, ¶m_types, &ref_params)?; let caller_stack_pad_bytes = direct_call_stack_pad_bytes(ctx, call_args.overflow_bytes); abi::emit_reserve_temporary_stack(ctx.emitter, caller_stack_pad_bytes); + let abi_param_types = abi_param_types_for_refs(¶m_types, &ref_params); + remap_platform_args_to_runtime_helper_regs( + ctx.emitter, + int_register_arg_count(&abi_param_types), + ); abi::emit_call_label( ctx.emitter, intrinsic @@ -3736,6 +3776,11 @@ fn lower_static_runtime_intrinsic( )?; let caller_stack_pad_bytes = direct_call_stack_pad_bytes(ctx, call_args.overflow_bytes); abi::emit_reserve_temporary_stack(ctx.emitter, caller_stack_pad_bytes); + let visible_abi_param_types = abi_param_types_for_refs(¶m_types, &callee_ref_params); + remap_platform_args_to_runtime_helper_regs( + ctx.emitter, + 1 + int_register_arg_count(&visible_abi_param_types), // +1 for the hidden called-class-id arg (always Int, 1 register) + ); abi::emit_call_label( ctx.emitter, intrinsic @@ -3811,9 +3856,8 @@ fn lower_callback_filter_accept_intrinsic( /// integer key. The helper's result register holds the value delivered by the /// next `send()`/`next()`, which becomes the SSA result of the yield. fn lower_generator_yield(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - let target = ctx.emitter.target; - let key_arg = abi::int_arg_reg_name(target, 0); - let value_arg = abi::int_arg_reg_name(target, 1); + let key_arg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); + let value_arg = abi::runtime_helper_int_arg_reg(ctx.emitter, 1); let result_reg = abi::int_result_reg(ctx.emitter); let n = inst.operands.len(); @@ -3837,7 +3881,7 @@ fn lower_generator_yield(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> R emit_value_as_owned_mixed(ctx, key)?; if key_arg != result_reg { ctx.emitter - .instruction(&format!("mov {}, {}", key_arg, result_reg)); // move the boxed key into the first argument register + .instruction(&format!("mov {}, {}", key_arg, result_reg)); // move the boxed key into the first argument register } } None => { @@ -3857,8 +3901,7 @@ fn lower_generator_yield(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> R /// reaching the backend, so the operand here is always a Generator/Traversable. fn lower_generator_yield_from(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { let operand = expect_operand(inst, 0)?; - let target = ctx.emitter.target; - let arg0 = abi::int_arg_reg_name(target, 0); + let arg0 = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); let result_reg = abi::int_result_reg(ctx.emitter); ctx.load_value_to_result(operand)?; // inner generator pointer (borrowed) if arg0 != result_reg { @@ -3915,6 +3958,11 @@ fn lower_generator_intrinsic( materialize_direct_call_args_with_refs(ctx, &inst.operands, ¶m_types, &ref_params)?; let caller_stack_pad_bytes = direct_call_stack_pad_bytes(ctx, call_args.overflow_bytes); abi::emit_reserve_temporary_stack(ctx.emitter, caller_stack_pad_bytes); + let abi_param_types = abi_param_types_for_refs(¶m_types, &ref_params); + remap_platform_args_to_runtime_helper_regs( + ctx.emitter, + int_register_arg_count(&abi_param_types), + ); let helper = intrinsic.runtime_helper().ok_or_else(|| { CodegenIrError::invalid_module(format!( "Generator intrinsic {:?} has no runtime helper", @@ -4236,7 +4284,7 @@ fn lower_fiber_start( abi::emit_push_result_value(ctx.emitter, &push_ty); } let overflow_bytes = abi::materialize_outgoing_args(ctx.emitter, &assignments); - let receiver_arg = abi::int_arg_reg_name(ctx.emitter.target, 0); + let receiver_arg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); ctx.load_value_to_reg(object, receiver_arg)?; emit_store_fiber_start_args(ctx, &assignments, args.len())?; abi::emit_call_label(ctx.emitter, "__rt_fiber_start"); @@ -4254,9 +4302,9 @@ fn lower_fiber_resume( fiber_single_optional_arg(ctx, inst.operands.get(1..).unwrap_or(&[]), "Fiber::resume")?; emit_optional_mixed_arg(ctx, value)?; abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); // preserve the boxed resume value while loading the receiver - let receiver_arg = abi::int_arg_reg_name(ctx.emitter.target, 0); + let receiver_arg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); ctx.load_value_to_reg(object, receiver_arg)?; - abi::emit_pop_reg(ctx.emitter, abi::int_arg_reg_name(ctx.emitter.target, 1)); // pass the boxed resume value as runtime helper argument 2 + abi::emit_pop_reg(ctx.emitter, abi::runtime_helper_int_arg_reg(ctx.emitter, 1)); // pass the boxed resume value as runtime helper argument 2 abi::emit_call_label(ctx.emitter, "__rt_fiber_resume"); store_if_result(ctx, inst) } @@ -4282,8 +4330,8 @@ fn lower_fiber_throw( ))); } abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); // preserve the Throwable while loading the Fiber receiver - ctx.load_value_to_reg(object, abi::int_arg_reg_name(ctx.emitter.target, 0))?; - abi::emit_pop_reg(ctx.emitter, abi::int_arg_reg_name(ctx.emitter.target, 1)); // pass the Throwable object as runtime helper argument 2 + ctx.load_value_to_reg(object, abi::runtime_helper_int_arg_reg(ctx.emitter, 0))?; + abi::emit_pop_reg(ctx.emitter, abi::runtime_helper_int_arg_reg(ctx.emitter, 1)); // pass the Throwable object as runtime helper argument 2 abi::emit_call_label(ctx.emitter, "__rt_fiber_throw"); store_if_result(ctx, inst) } @@ -4393,7 +4441,7 @@ fn lower_fiber_noarg_runtime_method( helper ))); } - let receiver_arg = abi::int_arg_reg_name(ctx.emitter.target, 0); + let receiver_arg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); ctx.load_value_to_reg(object, receiver_arg)?; abi::emit_call_label(ctx.emitter, helper); store_if_result(ctx, inst) @@ -4484,7 +4532,7 @@ fn lower_fiber_state_predicate( object: ValueId, state: FiberStatePredicate, ) -> Result<()> { - let receiver_arg = abi::int_arg_reg_name(ctx.emitter.target, 0); + let receiver_arg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); ctx.load_value_to_reg(object, receiver_arg)?; emit_fiber_state_predicate_call(ctx, inst, state) } @@ -4523,7 +4571,7 @@ fn emit_mixed_fiber_receiver_to_arg( .ok_or_else(|| { CodegenIrError::unsupported("mixed Fiber predicate without Fiber metadata") })?; - let receiver_arg = abi::int_arg_reg_name(ctx.emitter.target, 0); + let receiver_arg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); ctx.load_value_to_reg(object, abi::int_result_reg(ctx.emitter))?; abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); match ctx.emitter.target.arch { @@ -4563,7 +4611,7 @@ fn emit_fiber_state_predicate_call( ) -> Result<()> { abi::emit_load_int_immediate( ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), + abi::runtime_helper_int_arg_reg(ctx.emitter, 1), state.expected_state() as i64, ); abi::emit_call_label(ctx.emitter, "__rt_fiber_state_eq"); @@ -5148,7 +5196,7 @@ fn lower_static_fiber_suspend(ctx: &mut FunctionContext<'_>, inst: &Instruction) let value = fiber_single_optional_arg(ctx, &inst.operands, "Fiber::suspend")?; emit_optional_mixed_arg(ctx, value)?; abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); // preserve the boxed suspend value for target-specific argument loading - abi::emit_pop_reg(ctx.emitter, abi::int_arg_reg_name(ctx.emitter.target, 0)); // pass the boxed suspend value as runtime helper argument 1 + abi::emit_pop_reg(ctx.emitter, abi::runtime_helper_int_arg_reg(ctx.emitter, 0)); // pass the boxed suspend value as runtime helper argument 1 abi::emit_call_label(ctx.emitter, "__rt_fiber_suspend"); store_if_result(ctx, inst) } @@ -5896,12 +5944,12 @@ fn emit_branch_if_cleanup_temp_aliases_result( match ctx.emitter.target.arch { Arch::AArch64 => { ctx.emitter - .instruction(&format!("cmp {}, {}", cleanup_reg, result_reg)); // compare the temporary Mixed cell with the saved call result + .instruction(&format!("cmp {}, {}", cleanup_reg, result_reg)); // compare the temporary Mixed cell with the saved call result ctx.emitter.instruction(&format!("b.eq {}", skip_label)); // keep the temp alive when ownership moved to the result } Arch::X86_64 => { ctx.emitter - .instruction(&format!("cmp {}, {}", cleanup_reg, result_reg)); // compare the temporary Mixed cell with the saved call result + .instruction(&format!("cmp {}, {}", cleanup_reg, result_reg)); // compare the temporary Mixed cell with the saved call result ctx.emitter.instruction(&format!("je {}", skip_label)); // keep the temp alive when ownership moved to the result } } @@ -6390,7 +6438,7 @@ pub(super) fn load_value_to_first_int_arg( pub(super) fn emit_mixed_string_for_persistent_store(ctx: &mut FunctionContext<'_>) { let non_string = ctx.next_label("mixed_string_persist_non_string"); let done = ctx.next_label("mixed_string_persist_done"); - let mixed_arg = abi::int_arg_reg_name(ctx.emitter.target, 0); + let mixed_arg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); abi::emit_push_reg(ctx.emitter, mixed_arg); abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); match ctx.emitter.target.arch { @@ -6454,7 +6502,7 @@ pub(super) fn resolve_int_operand_to_result( /// Moves the canonical integer result register into the target's first argument register. fn move_int_result_to_first_arg(ctx: &mut FunctionContext<'_>) { let result_reg = abi::int_result_reg(ctx.emitter); - let arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); + let arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); if result_reg == arg_reg { return; } @@ -6470,7 +6518,65 @@ fn move_int_result_to_first_arg(ctx: &mut FunctionContext<'_>) { } } +/// Remaps the platform (MSx64) integer argument registers into the SysV +/// registers a hand-written `__rt_*` runtime helper reads, on the +/// windows-x86_64 target. No-op (byte-identical output) on every other +/// target because `int_arg_reg_name` and `runtime_helper_int_arg_reg` +/// resolve to the same register there; the register choice only diverges +/// on windows-x86_64 (see `runtime_helper_int_arg_reg`'s docs). Only valid +/// when every argument is register-passed (`arg_count <= 4`, i.e. no +/// caller-stack overflow args) — the generator/instance/static +/// runtime-intrinsic dispatch families this serves are all small-arity. +/// Panics rather than silently miscompiling a future >4-register-arg +/// caller; extend this helper deliberately if that ever happens. +fn remap_platform_args_to_runtime_helper_regs( + emitter: &mut crate::codegen::emit::Emitter, + arg_count: usize, +) { + assert!( + arg_count <= 4, + "runtime-helper register remap only covers <=4 register-passed args, got {}", + arg_count + ); + if emitter.target.platform == Platform::Windows && emitter.target.arch == Arch::X86_64 { + for idx in 0..arg_count { + let src = abi::int_arg_reg_name(emitter.target, idx); + let dst = abi::runtime_helper_int_arg_reg(emitter, idx); + if src != dst { + emitter.instruction(&format!("mov {}, {}", dst, src)); // MSx64 arg -> SysV arg for the hand-written helper + } + } + } +} + +/// Returns the number of integer-class ABI argument registers consumed by +/// `abi_param_types`, mirroring the register-class accounting used by +/// `build_outgoing_arg_assignments_for_target`/`materialize_outgoing_args`: +/// float-class types occupy separate XMM registers (byte-identical index on +/// every x86_64 target, so they never need remapping), and multi-slot +/// int-class types (`Str`'s pointer+length pair) occupy `register_count()` +/// consecutive integer registers rather than one. Passing `param_types.len()` +/// directly to the remap helper undercounts whenever a `Str` (or other +/// 2-register) argument is present. +fn int_register_arg_count(abi_param_types: &[PhpType]) -> usize { + abi_param_types + .iter() + .filter(|ty| !ty.is_float_reg()) + .map(|ty| ty.register_count()) + .sum() +} + /// Returns the temporary caller-stack pad needed to match incoming stack-arg offsets. +/// +/// AArch64 call sites reserve a fixed 16-byte nested-call save slot above outgoing +/// stack args (see `caller_stack_start_offset`'s doc comment). x86_64 callees read +/// their first stack-passed argument at `caller_stack_start_offset(target)` bytes +/// above their frame pointer; on Linux/macOS that offset (16: 8-byte return address +/// + 8-byte saved rbp) already matches where this function's stack-arg staging +/// lands the overflow args, so no extra pad is needed. Windows x86_64 callees read +/// stack args starting at offset 48 after saving rbp (32-byte mandatory shadow +/// space + 8-byte return address + 8-byte saved frame pointer), so the caller +/// must reserve the shadow area or the callee reads the wrong stack slot. pub(super) fn direct_call_stack_pad_bytes( ctx: &FunctionContext<'_>, overflow_bytes: usize, @@ -7031,7 +7137,7 @@ fn coerce_ref_cell_store_value( // Save the Mixed pointer on the stack, narrow to int, then // release the Mixed box to avoid leaking the checked-arithmetic temporary. move_int_result_to_first_arg(ctx); - let arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); + let arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); let result_reg = abi::int_result_reg(ctx.emitter); // Stack layout after pushes: [result_placeholder | saved_mixed_ptr] abi::emit_push_reg(ctx.emitter, result_reg); // placeholder for int result @@ -7055,7 +7161,7 @@ fn coerce_ref_cell_store_value( } PhpType::Bool => { move_int_result_to_first_arg(ctx); - let arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); + let arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); let result_reg = abi::int_result_reg(ctx.emitter); abi::emit_push_reg(ctx.emitter, result_reg); abi::emit_push_reg(ctx.emitter, arg_reg); @@ -7075,7 +7181,7 @@ fn coerce_ref_cell_store_value( } PhpType::Float => { move_int_result_to_first_arg(ctx); - let arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); + let arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); let int_reg = abi::int_result_reg(ctx.emitter); abi::emit_push_reg(ctx.emitter, int_reg); // placeholder for float result abi::emit_push_reg(ctx.emitter, arg_reg); // save Mixed pointer @@ -7689,3 +7795,64 @@ fn expect_operand(inst: &Instruction, index: usize) -> Result { CodegenIrError::invalid_module(format!("{} missing operand {}", inst.op.name(), index)) }) } + +#[cfg(test)] +mod tests { + use crate::codegen::emit::Emitter; + use crate::codegen::platform::Target; + + use super::*; + + /// Verifies that on windows-x86_64 the remap emits `mov` instructions moving + /// every MSx64 argument register into the SysV register the hand-written + /// `__rt_*` helper actually reads, for a 2-register-arg call. + #[test] + fn test_remap_emits_msx64_to_sysv_on_windows_x86_64() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + remap_platform_args_to_runtime_helper_regs(&mut emitter, 2); + let asm = emitter.output(); + assert!(asm.contains("mov rdi, rcx")); + assert!(asm.contains("mov rsi, rdx")); + } + + /// Verifies that on linux-x86_64 the remap is a no-op: `int_arg_reg_name` + /// and `runtime_helper_int_arg_reg` already agree there, so `src == dst` + /// for every index and nothing is emitted. + #[test] + fn test_remap_is_noop_on_linux_x86_64() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + remap_platform_args_to_runtime_helper_regs(&mut emitter, 4); + let asm = emitter.output(); + assert!(!asm.contains("mov rdi, rcx")); + assert!(!asm.contains("mov")); + } + + /// Verifies that on macOS AArch64 the remap emits nothing: AArch64 has a + /// single calling convention, so `int_arg_reg_name` and + /// `runtime_helper_int_arg_reg` are byte-identical for every index. + #[test] + fn test_remap_is_noop_on_aarch64() { + let mut emitter = Emitter::new(Target::new(Platform::MacOS, Arch::AArch64)); + remap_platform_args_to_runtime_helper_regs(&mut emitter, 2); + let asm = emitter.output(); + assert!(asm.is_empty()); + } + + /// Verifies that a `Str` (pointer+length pair) argument counts as 2 + /// integer-class registers, not 1 — the bug this helper must avoid for + /// call sites like `SplDoublyLinkedList::unserialize(string $data)`. + #[test] + fn test_int_register_arg_count_counts_str_as_two_registers() { + let abi_param_types = vec![PhpType::Object("SplDoublyLinkedList".to_string()), PhpType::Str]; + assert_eq!(int_register_arg_count(&abi_param_types), 3); + } + + /// Verifies that float-class arguments are excluded from the integer + /// register count, since they occupy separate XMM registers that never + /// need remapping between MSx64 and SysV. + #[test] + fn test_int_register_arg_count_excludes_float_args() { + let abi_param_types = vec![PhpType::Int, PhpType::Float, PhpType::Int]; + assert_eq!(int_register_arg_count(&abi_param_types), 2); + } +} diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index 444d2f9463..5865e807d0 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -16,7 +16,9 @@ use crate::codegen::platform::Arch; use crate::ir::{Immediate, Instruction, Op, PhpTypePredicate, ValueDef, ValueId}; use crate::names::{define_seen_symbol, ir_global_symbol, php_symbol_key}; use crate::parser::ast::Visibility; -use crate::types::checker::builtins::is_php_visible_builtin_function; +use crate::types::checker::builtins::{ + builtin_available_on_platform, is_php_visible_builtin_function, +}; use crate::types::{ClassInfo, PhpType}; use super::super::context::FunctionContext; @@ -554,7 +556,7 @@ pub(crate) fn lower_function_exists(ctx: &mut FunctionContext<'_>, inst: &Instru } else { let exists = ctx.function_by_name(&function_name).is_some() || ctx.has_extern_function(&function_name) - || is_php_visible_builtin_function(function_name.trim_start_matches('\\')) + || php_visible_builtin_exists_on_target(ctx, &function_name) || crate::name_resolver::is_date_procedural_alias(&function_name); emit_static_bool(ctx, exists); } @@ -1554,7 +1556,14 @@ fn callable_name_exists(ctx: &FunctionContext<'_>, name: &str) -> bool { ctx.function_variant_group_name(name).is_some() || ctx.function_by_name(name).is_some() || ctx.has_extern_function(name) - || is_php_visible_builtin_function(name.trim_start_matches('\\')) + || php_visible_builtin_exists_on_target(ctx, name) +} + +/// Returns whether a PHP-visible builtin is registered on the active target. +fn php_visible_builtin_exists_on_target(ctx: &FunctionContext<'_>, name: &str) -> bool { + let bare = name.trim_start_matches('\\'); + is_php_visible_builtin_function(bare) + && builtin_available_on_platform(&php_symbol_key(bare), ctx.emitter.target.platform) } /// Checks whether a PHP symbol is present in an iterator of known names. diff --git a/src/codegen/lower_inst/builtins/arrays.rs b/src/codegen/lower_inst/builtins/arrays.rs index ddafb770bb..4bdedf662f 100644 --- a/src/codegen/lower_inst/builtins/arrays.rs +++ b/src/codegen/lower_inst/builtins/arrays.rs @@ -379,9 +379,9 @@ pub(crate) fn lower_array_map(ctx: &mut FunctionContext<'_>, inst: &Instruction) PhpType::Mixed, "array_map", |ctx, wrapper_label, env_bytes| { - let callback_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); - let array_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 1); - let env_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 2); + let callback_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); + let array_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 1); + let env_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 2); abi::emit_symbol_address(ctx.emitter, callback_arg_reg, wrapper_label); ctx.load_value_to_reg(array, array_arg_reg)?; load_static_callback_env_arg(ctx, env_arg_reg, env_bytes); @@ -430,9 +430,9 @@ pub(crate) fn lower_array_map(ctx: &mut FunctionContext<'_>, inst: &Instruction) let callback_elem_ty = array_map_callback_result_element_type(&callback_binding.return_ty)?; let result_elem_ty = array_map_result_element_type(inst, &callback_elem_ty)?; let env_bytes = reserve_static_callback_env(ctx, callback_binding.env_source)?; - let callback_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); - let array_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 1); - let env_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 2); + let callback_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); + let array_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 1); + let env_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 2); abi::emit_symbol_address(ctx.emitter, callback_arg_reg, &callback_binding.label); ctx.load_value_to_reg(array, array_arg_reg)?; load_static_callback_env_arg(ctx, env_arg_reg, env_bytes); @@ -467,9 +467,9 @@ fn lower_array_map_descriptor_callback( let wrapper_label = emit_descriptor_callback_wrapper(ctx, vec![elem_ty.clone()], callback_elem_ty.clone()); let env_bytes = reserve_descriptor_callback_env(ctx, callback)?; - let callback_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); - let array_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 1); - let env_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 2); + let callback_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); + let array_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 1); + let env_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 2); abi::emit_symbol_address(ctx.emitter, callback_arg_reg, &wrapper_label); ctx.load_value_to_reg(array, array_arg_reg)?; load_static_callback_env_arg(ctx, env_arg_reg, env_bytes); @@ -502,9 +502,9 @@ fn lower_array_map_callable_array_descriptor_callback( )?; let descriptor_reg = abi::int_result_reg(ctx.emitter); let env_bytes = reserve_descriptor_callback_env_from_reg(ctx, descriptor_reg); - let callback_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); - let array_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 1); - let env_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 2); + let callback_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); + let array_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 1); + let env_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 2); abi::emit_symbol_address(ctx.emitter, callback_arg_reg, &wrapper_label); ctx.load_value_to_reg(array, array_arg_reg)?; load_static_callback_env_arg(ctx, env_arg_reg, env_bytes); @@ -740,10 +740,10 @@ pub(crate) fn lower_array_reduce(ctx: &mut FunctionContext<'_>, inst: &Instructi vec![initial_ty.clone(), elem_ty.clone()], PhpType::Int, |ctx, wrapper_label, env_bytes| { - let callback_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); - let array_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 1); - let initial_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 2); - let env_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 3); + let callback_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); + let array_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 1); + let initial_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 2); + let env_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 3); abi::emit_symbol_address(ctx.emitter, callback_arg_reg, wrapper_label); ctx.load_value_to_reg(array, array_arg_reg)?; ctx.load_value_to_reg(initial, initial_arg_reg)?; @@ -765,10 +765,10 @@ pub(crate) fn lower_array_reduce(ctx: &mut FunctionContext<'_>, inst: &Instructi PhpType::Int, "array_reduce", |ctx, wrapper_label, env_bytes| { - let callback_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); - let array_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 1); - let initial_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 2); - let env_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 3); + let callback_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); + let array_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 1); + let initial_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 2); + let env_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 3); abi::emit_symbol_address(ctx.emitter, callback_arg_reg, wrapper_label); ctx.load_value_to_reg(array, array_arg_reg)?; ctx.load_value_to_reg(initial, initial_arg_reg)?; @@ -790,10 +790,10 @@ pub(crate) fn lower_array_reduce(ctx: &mut FunctionContext<'_>, inst: &Instructi Some(&[initial_ty.clone(), elem_ty]), )?; let env_bytes = reserve_static_callback_env(ctx, callback_binding.env_source)?; - let callback_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); - let array_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 1); - let initial_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 2); - let env_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 3); + let callback_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); + let array_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 1); + let initial_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 2); + let env_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 3); abi::emit_symbol_address(ctx.emitter, callback_arg_reg, &callback_binding.label); ctx.load_value_to_reg(array, array_arg_reg)?; ctx.load_value_to_reg(initial, initial_arg_reg)?; @@ -820,9 +820,9 @@ pub(crate) fn lower_array_walk(ctx: &mut FunctionContext<'_>, inst: &Instruction vec![elem_ty.clone()], PhpType::Void, |ctx, wrapper_label, env_bytes| { - let callback_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); - let array_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 1); - let env_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 2); + let callback_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); + let array_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 1); + let env_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 2); abi::emit_symbol_address(ctx.emitter, callback_arg_reg, wrapper_label); ctx.load_value_to_reg(array, array_arg_reg)?; load_static_callback_env_arg(ctx, env_arg_reg, env_bytes); @@ -842,9 +842,9 @@ pub(crate) fn lower_array_walk(ctx: &mut FunctionContext<'_>, inst: &Instruction PhpType::Void, "array_walk", |ctx, wrapper_label, env_bytes| { - let callback_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); - let array_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 1); - let env_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 2); + let callback_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); + let array_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 1); + let env_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 2); abi::emit_symbol_address(ctx.emitter, callback_arg_reg, wrapper_label); ctx.load_value_to_reg(array, array_arg_reg)?; load_static_callback_env_arg(ctx, env_arg_reg, env_bytes); @@ -860,9 +860,9 @@ pub(crate) fn lower_array_walk(ctx: &mut FunctionContext<'_>, inst: &Instruction let callback_binding = static_sort_callback_binding(ctx, callback, "array_walk callback", Some(&[elem_ty]))?; let env_bytes = reserve_static_callback_env(ctx, callback_binding.env_source)?; - let callback_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); - let array_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 1); - let env_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 2); + let callback_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); + let array_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 1); + let env_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 2); abi::emit_symbol_address(ctx.emitter, callback_arg_reg, &callback_binding.label); ctx.load_value_to_reg(array, array_arg_reg)?; load_static_callback_env_arg(ctx, env_arg_reg, env_bytes); @@ -1182,7 +1182,7 @@ pub(crate) fn lower_array_is_list(ctx: &mut FunctionContext<'_>, inst: &Instruct super::ensure_arg_count(inst, "array_is_list", 1)?; let array = expect_operand(inst, 0)?; require_array_like_operand(ctx.value_php_type(array)?, "array_is_list")?; - let arg0 = abi::int_arg_reg_name(ctx.emitter.target, 0); + let arg0 = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); ctx.load_value_to_reg(array, arg0)?; abi::emit_call_label(ctx.emitter, "__rt_array_is_list"); store_if_result(ctx, inst) @@ -1218,8 +1218,8 @@ fn lower_array_edge_key( super::ensure_arg_count(inst, name, 1)?; let array = expect_operand(inst, 0)?; require_array_like_operand(ctx.value_php_type(array)?, name)?; - let arg0 = abi::int_arg_reg_name(ctx.emitter.target, 0); - let arg1 = abi::int_arg_reg_name(ctx.emitter.target, 1); + let arg0 = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); + let arg1 = abi::runtime_helper_int_arg_reg(ctx.emitter, 1); ctx.load_value_to_reg(array, arg0)?; abi::emit_load_int_immediate(ctx.emitter, arg1, which); abi::emit_call_label(ctx.emitter, "__rt_array_edge_key"); @@ -2098,9 +2098,9 @@ fn lower_user_sort_static_callback( callback_arg_types.to_vec(), PhpType::Int, |ctx, wrapper_label, env_bytes| { - let callback_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); - let array_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 1); - let env_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 2); + let callback_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); + let array_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 1); + let env_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 2); abi::emit_symbol_address(ctx.emitter, callback_arg_reg, wrapper_label); ctx.load_value_to_reg(array, array_arg_reg)?; load_static_callback_env_arg(ctx, env_arg_reg, env_bytes); @@ -2125,9 +2125,9 @@ fn lower_user_sort_static_callback( PhpType::Int, name, |ctx, wrapper_label, env_bytes| { - let callback_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); - let array_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 1); - let env_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 2); + let callback_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); + let array_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 1); + let env_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 2); abi::emit_symbol_address(ctx.emitter, callback_arg_reg, wrapper_label); ctx.load_value_to_reg(array, array_arg_reg)?; load_static_callback_env_arg(ctx, env_arg_reg, env_bytes); @@ -2163,9 +2163,9 @@ fn lower_user_sort_with_static_callback_binding( ) -> Result<()> { let callback_label = sort_callback_label_returning_int(ctx, &callback_binding)?; let env_bytes = reserve_static_callback_env(ctx, callback_binding.env_source)?; - let callback_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); - let array_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 1); - let env_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 2); + let callback_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); + let array_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 1); + let env_arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 2); abi::emit_symbol_address(ctx.emitter, callback_arg_reg, &callback_label); ctx.load_value_to_reg(array, array_arg_reg)?; load_static_callback_env_arg(ctx, env_arg_reg, env_bytes); @@ -2233,7 +2233,7 @@ fn emit_sort_callback_mixed_return_int_adapter( /// Casts the current owned Mixed result to int and releases the consumed Mixed cell. fn emit_owned_mixed_result_cast_to_int(ctx: &mut FunctionContext<'_>) { move_sort_callback_int_result_to_first_arg(ctx); - let arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); + let arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); let result_reg = abi::int_result_reg(ctx.emitter); abi::emit_push_reg(ctx.emitter, result_reg); abi::emit_push_reg(ctx.emitter, arg_reg); @@ -2254,7 +2254,7 @@ fn emit_owned_mixed_result_cast_to_int(ctx: &mut FunctionContext<'_>) { /// Moves the integer result register into the first argument register when required. fn move_sort_callback_int_result_to_first_arg(ctx: &mut FunctionContext<'_>) { let result_reg = abi::int_result_reg(ctx.emitter); - let arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); + let arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); if result_reg == arg_reg { return; } @@ -3916,6 +3916,41 @@ fn release_callback_boxed_args(ctx: &mut FunctionContext<'_>, frame: &CallbackWr } } +/// Releases wrapper-owned Mixed argument boxes except the box transferred as the call result. +fn release_callback_boxed_args_except_return_alias( + ctx: &mut FunctionContext<'_>, + frame: &CallbackWrapperFrame, +) { + for offset in &frame.boxed_offsets { + if *offset == NO_CALLBACK_BOX_OFFSET { + continue; + } + let skip_label = ctx.next_label("callback_return_alias_cleanup_skip"); + let result_reg = abi::int_result_reg(ctx.emitter); + let return_reg = abi::temp_int_reg(ctx.emitter.target); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, *offset); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + return_reg, + frame.return_offset, + ); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter + .instruction(&format!("cmp {}, {}", result_reg, return_reg)); // check whether this wrapper-owned box became the callback result + ctx.emitter.instruction(&format!("b.eq {}", skip_label)); // transfer the matching wrapper reference to the callback caller + } + Arch::X86_64 => { + ctx.emitter + .instruction(&format!("cmp {}, {}", result_reg, return_reg)); // check whether this wrapper-owned box became the callback result + ctx.emitter.instruction(&format!("je {}", skip_label)); // transfer the matching wrapper reference to the callback caller + } + } + abi::emit_decref_if_refcounted(ctx.emitter, &PhpType::Mixed); + ctx.emitter.label(&skip_label); + } +} + /// Cleans up boxed callback arguments without losing the callback return value. fn cleanup_callback_boxed_args( ctx: &mut FunctionContext<'_>, @@ -3925,11 +3960,12 @@ fn cleanup_callback_boxed_args( if !callback_wrapper_has_boxed_args(frame) { return; } + save_callback_return_value(ctx, frame, return_ty); if callback_return_may_alias_boxed_args(return_ty) { - return; + release_callback_boxed_args_except_return_alias(ctx, frame); + } else { + release_callback_boxed_args(ctx, frame); } - save_callback_return_value(ctx, frame, return_ty); - release_callback_boxed_args(ctx, frame); restore_callback_return_value(ctx, frame, return_ty); } @@ -3967,7 +4003,10 @@ fn emit_static_method_callback_wrapper_aarch64( target: &StaticMethodCallbackTarget, visible_arg_types: &[PhpType], ) { - let env_reg = abi::int_arg_reg_name(ctx.emitter.target, callback_arg_abi_slots(visible_arg_types)); + let env_reg = abi::int_arg_reg_name( + ctx.emitter.target, + callback_arg_abi_slots(visible_arg_types), + ); let frame = callback_wrapper_frame(&target.param_types, visible_arg_types); abi::emit_reserve_temporary_stack(ctx.emitter, frame.total_bytes); abi::emit_store_to_sp(ctx.emitter, "x30", frame.return_address_offset); @@ -3996,7 +4035,10 @@ fn emit_static_method_callback_wrapper_x86_64( target: &StaticMethodCallbackTarget, visible_arg_types: &[PhpType], ) { - let env_reg = abi::int_arg_reg_name(ctx.emitter.target, callback_arg_abi_slots(visible_arg_types)); + let env_reg = abi::int_arg_reg_name( + ctx.emitter.target, + callback_arg_abi_slots(visible_arg_types), + ); let frame = callback_wrapper_frame(&target.param_types, visible_arg_types); ctx.emitter.instruction("push rbp"); // preserve the runtime helper frame pointer for the nested static method call ctx.emitter.instruction("mov rbp, rsp"); // establish a wrapper frame while shifting callback arguments @@ -4013,7 +4055,10 @@ fn emit_static_method_callback_wrapper_x86_64( save_callback_hidden_arg(ctx, &frame, "rcx"); box_callback_mixed_args(ctx, &frame, &target.param_types, visible_arg_types); load_callback_target_args(ctx, &frame, visible_arg_types); + let call_pad_bytes = abi::outgoing_call_stack_pad_bytes(ctx.emitter.target, 0); + abi::emit_reserve_temporary_stack(ctx.emitter, call_pad_bytes); emit_static_callback_dispatch(ctx, target); + abi::emit_release_temporary_stack(ctx.emitter, call_pad_bytes); cleanup_callback_boxed_args(ctx, &frame, &target.return_ty); abi::emit_release_temporary_stack(ctx.emitter, frame.total_bytes); ctx.emitter.instruction("pop rbp"); // restore the runtime helper frame pointer before returning @@ -4048,7 +4093,10 @@ fn emit_instance_method_callback_wrapper_aarch64( target: &InstanceMethodCallbackTarget, visible_arg_types: &[PhpType], ) { - let env_reg = abi::int_arg_reg_name(ctx.emitter.target, callback_arg_abi_slots(visible_arg_types)); + let env_reg = abi::int_arg_reg_name( + ctx.emitter.target, + callback_arg_abi_slots(visible_arg_types), + ); let frame = callback_wrapper_frame(&target.param_types, visible_arg_types); abi::emit_reserve_temporary_stack(ctx.emitter, frame.total_bytes); abi::emit_store_to_sp(ctx.emitter, "x30", frame.return_address_offset); @@ -4070,7 +4118,10 @@ fn emit_instance_method_callback_wrapper_x86_64( target: &InstanceMethodCallbackTarget, visible_arg_types: &[PhpType], ) { - let env_reg = abi::int_arg_reg_name(ctx.emitter.target, callback_arg_abi_slots(visible_arg_types)); + let env_reg = abi::int_arg_reg_name( + ctx.emitter.target, + callback_arg_abi_slots(visible_arg_types), + ); let frame = callback_wrapper_frame(&target.param_types, visible_arg_types); ctx.emitter.instruction("push rbp"); // preserve the runtime helper frame pointer for the nested instance method call ctx.emitter.instruction("mov rbp, rsp"); // establish a wrapper frame while shifting callback arguments @@ -4080,7 +4131,10 @@ fn emit_instance_method_callback_wrapper_x86_64( save_callback_hidden_arg(ctx, &frame, "rcx"); box_callback_mixed_args(ctx, &frame, &target.param_types, visible_arg_types); load_callback_target_args(ctx, &frame, visible_arg_types); + let call_pad_bytes = abi::outgoing_call_stack_pad_bytes(ctx.emitter.target, 0); + abi::emit_reserve_temporary_stack(ctx.emitter, call_pad_bytes); abi::emit_call_label(ctx.emitter, &target.entry_label); + abi::emit_release_temporary_stack(ctx.emitter, call_pad_bytes); cleanup_callback_boxed_args(ctx, &frame, &target.return_ty); abi::emit_release_temporary_stack(ctx.emitter, frame.total_bytes); ctx.emitter.instruction("pop rbp"); // restore the runtime helper frame pointer before returning @@ -5362,16 +5416,15 @@ fn lower_array_pad_call( Ok(()) } -/// Emits the `-1` runtime sentinel used when slicing to the end of the source array. +/// Emits the runtime sentinel used when slicing to the end of the source array. +/// +/// `i64::MAX` rather than `-1`: the helpers clamp the requested length down to the +/// remaining window, so the largest possible value already means "until the end" and +/// needs no test of its own. `-1` could not serve, because php gives it a meaning — +/// stop one element before the end — which made an explicit `-1` indistinguishable +/// from an omitted argument. fn emit_array_slice_until_end_sentinel(ctx: &mut FunctionContext<'_>, reg: &str) { - match ctx.emitter.target.arch { - Arch::AArch64 => { - ctx.emitter.instruction(&format!("mov {}, #-1", reg)); // use -1 as the array_slice() runtime sentinel for length until the end - } - Arch::X86_64 => { - ctx.emitter.instruction(&format!("mov {}, -1", reg)); // use -1 as the x86_64 array_slice() runtime sentinel for length until the end - } - } + abi::emit_load_int_immediate(ctx.emitter, reg, i64::MAX); } /// Returns the helper that matches the chunk source element ownership representation. diff --git a/src/codegen/lower_inst/builtins/debug.rs b/src/codegen/lower_inst/builtins/debug.rs index 1622b9cd03..e2514d01df 100644 --- a/src/codegen/lower_inst/builtins/debug.rs +++ b/src/codegen/lower_inst/builtins/debug.rs @@ -419,9 +419,14 @@ fn emit_var_dump_int_payload(ctx: &mut FunctionContext<'_>) { } /// Emits `var_dump` output for a float payload in the floating result register. +/// +/// `var_dump` renders floats at PHP's `serialize_precision = -1` (shortest +/// round-trip), the same precision `json_encode`/`serialize` use — NOT the +/// `precision = 14` setting `__rt_ftoa` implements for `echo`/`(string)`. This +/// calls the Inf/NaN-safe `__rt_var_dump_ftoa` wrapper instead of `__rt_ftoa`. fn emit_var_dump_float(ctx: &mut FunctionContext<'_>) -> Result<()> { let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); - abi::emit_call_label(ctx.emitter, "__rt_ftoa"); + abi::emit_call_label(ctx.emitter, "__rt_var_dump_ftoa"); abi::emit_push_reg_pair(ctx.emitter, ptr_reg, len_reg); emit_write_literal(ctx, b"float("); abi::emit_pop_reg_pair(ctx.emitter, ptr_reg, len_reg); diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index c58c277c6d..370147c6e4 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -275,13 +275,18 @@ pub(super) fn lower_eval(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> R mark_eval_scope_global_aliases(ctx, &global_aliases); set_eval_context_global_scope(ctx); let pushed_class_scope = push_eval_context_class_scope(ctx)?; - load_eval_context_to_arg(ctx, 0); - load_eval_scope_to_arg(ctx, 1); - move_saved_eval_code_to_eval_args(ctx); - let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); - abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_execute"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::TemporaryStackValue(EVAL_CONTEXT_HANDLE_OFFSET), + EvalCAbiArg::TemporaryStackValue(EVAL_SCOPE_HANDLE_OFFSET), + EvalCAbiArg::TemporaryStackValue(EVAL_CODE_PTR_OFFSET), + EvalCAbiArg::TemporaryStackValue(EVAL_CODE_LEN_OFFSET), + EvalCAbiArg::TemporaryStackAddress(0), + ], + ); pop_eval_context_class_scope(ctx, pushed_class_scope); emit_eval_status_check(ctx); let result_reg = abi::int_result_reg(ctx.emitter); @@ -694,44 +699,38 @@ fn emit_eval_scope_get_for_loaded_scope( out_flags_offset: usize, ) { let (name_label, name_len) = ctx.data.add_string(name.as_bytes()); - let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); - abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - name_len as i64, - ); - let out_cell_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); - abi::emit_temporary_stack_address(ctx.emitter, out_cell_arg, out_cell_offset); - let out_flags_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); - abi::emit_temporary_stack_address(ctx.emitter, out_flags_arg, out_flags_offset); let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_get"); - abi::emit_call_label(ctx.emitter, &symbol); + let scope_arg = abi::int_arg_reg_name(ctx.emitter.target, 0); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::Register(scope_arg), + EvalCAbiArg::Symbol(&name_label), + EvalCAbiArg::Immediate(name_len as i64), + EvalCAbiArg::TemporaryStackAddress(out_cell_offset), + EvalCAbiArg::TemporaryStackAddress(out_flags_offset), + ], + ); emit_eval_status_check(ctx); } /// Calls `__elephc_eval_scope_set` using an already-loaded scope handle arg. fn emit_eval_scope_set_for_loaded_scope(ctx: &mut FunctionContext<'_>, name: &str, flags: i64) { let (name_label, name_len) = ctx.data.add_string(name.as_bytes()); - let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); - abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - name_len as i64, - ); - abi::emit_load_temporary_stack_slot( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - EVAL_TEMP_CELL_OFFSET, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - flags, - ); let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_set"); - abi::emit_call_label(ctx.emitter, &symbol); + let scope_arg = abi::int_arg_reg_name(ctx.emitter.target, 0); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::Register(scope_arg), + EvalCAbiArg::Symbol(&name_label), + EvalCAbiArg::Immediate(name_len as i64), + EvalCAbiArg::TemporaryStackValue(EVAL_TEMP_CELL_OFFSET), + EvalCAbiArg::Immediate(flags), + ], + ); emit_eval_status_check(ctx); } @@ -782,41 +781,32 @@ fn set_eval_call_site(ctx: &mut FunctionContext<'_>, inst: &Instruction) { let Some(source_path) = ctx.module.source_path.as_deref() else { return; }; - load_eval_context_to_arg(ctx, 0); let (file_label, file_len) = ctx.data.add_string(source_path.as_bytes()); - let file_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); - abi::emit_symbol_address(ctx.emitter, file_arg, &file_label); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - file_len as i64, - ); let dir = Path::new(source_path) .parent() .map(|path| path.display().to_string()) .unwrap_or_default(); let (dir_label, dir_len) = ctx.data.add_string(dir.as_bytes()); - let dir_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); - abi::emit_symbol_address(ctx.emitter, dir_arg, &dir_label); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - dir_len as i64, - ); let line = inst .span .and_then(|span| i64::try_from(span.line).ok()) .unwrap_or(0); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 5), - line, - ); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_context_set_call_site"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::TemporaryStackValue(EVAL_CONTEXT_HANDLE_OFFSET), + EvalCAbiArg::Symbol(&file_label), + EvalCAbiArg::Immediate(file_len as i64), + EvalCAbiArg::Symbol(&dir_label), + EvalCAbiArg::Immediate(dir_len as i64), + EvalCAbiArg::Immediate(line), + ], + ); emit_eval_status_check(ctx); } @@ -831,33 +821,28 @@ pub(super) fn lower_eval_function_call( abi::emit_reserve_temporary_stack(ctx.emitter, stack_bytes); ensure_eval_context(ctx)?; store_eval_function_call_args(ctx, inst, args_offset)?; - load_eval_context_to_arg(ctx, 0); let (name_label, name_len) = ctx.data.add_string(function_name.as_bytes()); - let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); - abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - name_len as i64, - ); - let args_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); - if inst.operands.is_empty() { - abi::emit_load_int_immediate(ctx.emitter, args_arg, 0); + let args_arg = if inst.operands.is_empty() { + EvalCAbiArg::Immediate(0) } else { - abi::emit_temporary_stack_address(ctx.emitter, args_arg, args_offset); - } - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - inst.operands.len() as i64, - ); - let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 5); - abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + EvalCAbiArg::TemporaryStackAddress(args_offset) + }; let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_call_function"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::TemporaryStackValue(EVAL_CONTEXT_HANDLE_OFFSET), + EvalCAbiArg::Symbol(&name_label), + EvalCAbiArg::Immediate(name_len as i64), + args_arg, + EvalCAbiArg::Immediate(inst.operands.len() as i64), + EvalCAbiArg::TemporaryStackAddress(0), + ], + ); emit_eval_status_check(ctx); let result_reg = abi::int_result_reg(ctx.emitter); abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); @@ -881,24 +866,22 @@ pub(super) fn lower_eval_function_call_array( } let result_reg = abi::int_result_reg(ctx.emitter); abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); - load_eval_context_to_arg(ctx, 0); let (name_label, name_len) = ctx.data.add_string(function_name.as_bytes()); - let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); - abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - name_len as i64, - ); - let args_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); - abi::emit_load_temporary_stack_slot(ctx.emitter, args_arg, EVAL_TEMP_CELL_OFFSET); - let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); - abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_call_function_array"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::TemporaryStackValue(EVAL_CONTEXT_HANDLE_OFFSET), + EvalCAbiArg::Symbol(&name_label), + EvalCAbiArg::Immediate(name_len as i64), + EvalCAbiArg::TemporaryStackValue(EVAL_TEMP_CELL_OFFSET), + EvalCAbiArg::TemporaryStackAddress(0), + ], + ); emit_eval_status_check(ctx); let result_reg = abi::int_result_reg(ctx.emitter); abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); @@ -917,29 +900,24 @@ pub(super) fn lower_eval_object_new( abi::emit_reserve_temporary_stack(ctx.emitter, stack_bytes); ensure_eval_context(ctx)?; store_eval_function_call_args(ctx, inst, args_offset)?; - load_eval_context_to_arg(ctx, 0); - let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); - abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - name_len as i64, - ); - let args_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); - if inst.operands.is_empty() { - abi::emit_load_int_immediate(ctx.emitter, args_arg, 0); + let args_arg = if inst.operands.is_empty() { + EvalCAbiArg::Immediate(0) } else { - abi::emit_temporary_stack_address(ctx.emitter, args_arg, args_offset); - } - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - inst.operands.len() as i64, - ); - let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 5); - abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + EvalCAbiArg::TemporaryStackAddress(args_offset) + }; let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_new_object"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::TemporaryStackValue(EVAL_CONTEXT_HANDLE_OFFSET), + EvalCAbiArg::Symbol(&name_label), + EvalCAbiArg::Immediate(name_len as i64), + args_arg, + EvalCAbiArg::Immediate(inst.operands.len() as i64), + EvalCAbiArg::TemporaryStackAddress(0), + ], + ); emit_eval_status_check(ctx); let result_reg = abi::int_result_reg(ctx.emitter); abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); @@ -969,29 +947,27 @@ pub(super) fn lower_eval_object_new_dynamic_fallback( abi::emit_store_to_sp(ctx.emitter, name_len_reg, EVAL_CODE_LEN_OFFSET); ensure_eval_context(ctx)?; store_eval_function_call_operands(ctx, constructor_args, args_offset)?; - load_eval_context_to_arg(ctx, 0); - let name_ptr_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); - abi::emit_load_temporary_stack_slot(ctx.emitter, name_ptr_arg, EVAL_CODE_PTR_OFFSET); - let name_len_arg = abi::int_arg_reg_name(ctx.emitter.target, 2); - abi::emit_load_temporary_stack_slot(ctx.emitter, name_len_arg, EVAL_CODE_LEN_OFFSET); - let args_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); - if constructor_args.is_empty() { - abi::emit_load_int_immediate(ctx.emitter, args_arg, 0); + let args_arg = if constructor_args.is_empty() { + EvalCAbiArg::Immediate(0) } else { - abi::emit_temporary_stack_address(ctx.emitter, args_arg, args_offset); - } - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - constructor_args.len() as i64, - ); - let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 5); - abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + EvalCAbiArg::TemporaryStackAddress(args_offset) + }; let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_try_new_object"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::TemporaryStackValue(EVAL_CONTEXT_HANDLE_OFFSET), + EvalCAbiArg::TemporaryStackValue(EVAL_CODE_PTR_OFFSET), + EvalCAbiArg::TemporaryStackValue(EVAL_CODE_LEN_OFFSET), + args_arg, + EvalCAbiArg::Immediate(constructor_args.len() as i64), + EvalCAbiArg::TemporaryStackAddress(0), + ], + ); emit_branch_if_eval_c_int_negative(ctx, &eval_miss_label); emit_eval_status_check(ctx); let result_reg = abi::int_result_reg(ctx.emitter); @@ -1026,26 +1002,23 @@ pub(super) fn lower_eval_method_call( let result_reg = abi::int_result_reg(ctx.emitter); abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); store_eval_method_call_arg_pack(ctx, inst, args_offset)?; - load_eval_context_to_arg(ctx, 0); - let object_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); - abi::emit_load_temporary_stack_slot(ctx.emitter, object_arg, EVAL_TEMP_CELL_OFFSET); let (method_label, method_len) = ctx.data.add_string(method_name.as_bytes()); - let method_ptr_arg = abi::int_arg_reg_name(ctx.emitter.target, 2); - abi::emit_symbol_address(ctx.emitter, method_ptr_arg, &method_label); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - method_len as i64, - ); - let pack_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); - abi::emit_temporary_stack_address(ctx.emitter, pack_arg, args_offset); - let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 5); - abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_method_call"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::TemporaryStackValue(EVAL_CONTEXT_HANDLE_OFFSET), + EvalCAbiArg::TemporaryStackValue(EVAL_TEMP_CELL_OFFSET), + EvalCAbiArg::Symbol(&method_label), + EvalCAbiArg::Immediate(method_len as i64), + EvalCAbiArg::TemporaryStackAddress(args_offset), + EvalCAbiArg::TemporaryStackAddress(0), + ], + ); emit_eval_status_check(ctx); let result_reg = abi::int_result_reg(ctx.emitter); abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); @@ -1065,25 +1038,23 @@ pub(super) fn lower_eval_static_method_call( abi::emit_reserve_temporary_stack(ctx.emitter, stack_bytes); ensure_eval_context(ctx)?; store_eval_static_method_call_arg_pack(ctx, inst, args_offset)?; - load_eval_context_to_arg(ctx, 0); let target = format!("{}::{}", class_name, method_name); let (target_label, target_len) = ctx.data.add_string(target.as_bytes()); - let target_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); - abi::emit_symbol_address(ctx.emitter, target_arg, &target_label); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - target_len as i64, - ); - let pack_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); - abi::emit_temporary_stack_address(ctx.emitter, pack_arg, args_offset); - let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); - abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_static_method_call"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::TemporaryStackValue(EVAL_CONTEXT_HANDLE_OFFSET), + EvalCAbiArg::Symbol(&target_label), + EvalCAbiArg::Immediate(target_len as i64), + EvalCAbiArg::TemporaryStackAddress(args_offset), + EvalCAbiArg::TemporaryStackAddress(0), + ], + ); emit_eval_status_check(ctx); let result_reg = abi::int_result_reg(ctx.emitter); abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); @@ -1107,36 +1078,23 @@ pub(super) fn lower_eval_native_frame_static_method_call( emit_eval_native_frame_override_probe(ctx, frame_class, &miss_stack_label); store_eval_static_method_call_arg_pack(ctx, inst, args_offset)?; let (frame_label, frame_len) = ctx.data.add_string(frame_class.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 0), - &frame_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - frame_len as i64, - ); let (method_label, method_len) = ctx.data.add_string(method_name.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - &method_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - method_len as i64, - ); - let pack_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); - abi::emit_temporary_stack_address(ctx.emitter, pack_arg, args_offset); - let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 5); - abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_native_frame_static_method_call"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::Symbol(&frame_label), + EvalCAbiArg::Immediate(frame_len as i64), + EvalCAbiArg::Symbol(&method_label), + EvalCAbiArg::Immediate(method_len as i64), + EvalCAbiArg::TemporaryStackAddress(args_offset), + EvalCAbiArg::TemporaryStackAddress(0), + ], + ); emit_branch_if_eval_c_int_negative(ctx, &miss_stack_label); emit_eval_status_check(ctx); let result_reg = abi::int_result_reg(ctx.emitter); @@ -1165,34 +1123,22 @@ pub(super) fn lower_eval_native_frame_static_property_get( abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); emit_eval_native_frame_override_probe(ctx, frame_class, &miss_stack_label); let (frame_label, frame_len) = ctx.data.add_string(frame_class.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 0), - &frame_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - frame_len as i64, - ); let (property_label, property_len) = ctx.data.add_string(property_name.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - &property_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - property_len as i64, - ); - let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); - abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_native_frame_static_property_get"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::Symbol(&frame_label), + EvalCAbiArg::Immediate(frame_len as i64), + EvalCAbiArg::Symbol(&property_label), + EvalCAbiArg::Immediate(property_len as i64), + EvalCAbiArg::TemporaryStackAddress(0), + ], + ); emit_branch_if_eval_c_int_negative(ctx, &miss_stack_label); emit_eval_status_check(ctx); let result_reg = abi::int_result_reg(ctx.emitter); @@ -1223,36 +1169,23 @@ pub(super) fn lower_eval_native_frame_static_property_set( emit_eval_native_frame_override_probe(ctx, frame_class, &miss_stack_label); store_eval_mixed_operand_at(ctx, value, EVAL_TEMP_CELL_OFFSET)?; let (frame_label, frame_len) = ctx.data.add_string(frame_class.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 0), - &frame_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - frame_len as i64, - ); let (property_label, property_len) = ctx.data.add_string(property_name.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - &property_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - property_len as i64, - ); - let value_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); - abi::emit_load_temporary_stack_slot(ctx.emitter, value_arg, EVAL_TEMP_CELL_OFFSET); - let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 5); - abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_native_frame_static_property_set"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::Symbol(&frame_label), + EvalCAbiArg::Immediate(frame_len as i64), + EvalCAbiArg::Symbol(&property_label), + EvalCAbiArg::Immediate(property_len as i64), + EvalCAbiArg::TemporaryStackValue(EVAL_TEMP_CELL_OFFSET), + EvalCAbiArg::TemporaryStackAddress(0), + ], + ); emit_branch_if_eval_c_int_negative(ctx, &miss_stack_label); emit_eval_status_check(ctx); abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); @@ -1275,18 +1208,20 @@ pub(super) fn lower_eval_callable_call_array( ensure_eval_context(ctx)?; store_eval_mixed_operand_at(ctx, callback, EVAL_TEMP_CELL_OFFSET)?; store_eval_mixed_operand_at(ctx, arg_array, EVAL_CALLABLE_ARG_ARRAY_OFFSET)?; - load_eval_context_to_arg(ctx, 0); - let callback_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); - abi::emit_load_temporary_stack_slot(ctx.emitter, callback_arg, EVAL_TEMP_CELL_OFFSET); - let arg_array_arg = abi::int_arg_reg_name(ctx.emitter.target, 2); - abi::emit_load_temporary_stack_slot(ctx.emitter, arg_array_arg, EVAL_CALLABLE_ARG_ARRAY_OFFSET); - let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); - abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_callable_call_array"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::TemporaryStackValue(EVAL_CONTEXT_HANDLE_OFFSET), + EvalCAbiArg::TemporaryStackValue(EVAL_TEMP_CELL_OFFSET), + EvalCAbiArg::TemporaryStackValue(EVAL_CALLABLE_ARG_ARRAY_OFFSET), + EvalCAbiArg::TemporaryStackAddress(0), + ], + ); emit_eval_status_check(ctx); let result_reg = abi::int_result_reg(ctx.emitter); abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); @@ -1303,14 +1238,18 @@ pub(super) fn lower_eval_is_callable( abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); ensure_eval_context(ctx)?; store_eval_mixed_operand_at(ctx, callback, EVAL_TEMP_CELL_OFFSET)?; - load_eval_context_to_arg(ctx, 0); - let callback_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); - abi::emit_load_temporary_stack_slot(ctx.emitter, callback_arg, EVAL_TEMP_CELL_OFFSET); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_is_callable"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::TemporaryStackValue(EVAL_CONTEXT_HANDLE_OFFSET), + EvalCAbiArg::TemporaryStackValue(EVAL_TEMP_CELL_OFFSET), + ], + ); abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); box_eval_bool_result_if_mixed(ctx, inst); store_if_result(ctx, inst) @@ -1443,27 +1382,22 @@ pub(super) fn lower_eval_object_is_a( store_eval_object_operand(ctx, object)?; abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); emit_branch_if_eval_unboxed_not_object(ctx, &false_label); - load_eval_context_to_arg(ctx, 0); - let object_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); - abi::emit_load_temporary_stack_slot(ctx.emitter, object_arg, EVAL_TEMP_CELL_OFFSET); let (target_label, target_len) = ctx.data.add_string(target_class.as_bytes()); - let target_arg = abi::int_arg_reg_name(ctx.emitter.target, 2); - abi::emit_symbol_address(ctx.emitter, target_arg, &target_label); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - target_len as i64, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - i64::from(exclude_self), - ); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_object_is_a"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::TemporaryStackValue(EVAL_CONTEXT_HANDLE_OFFSET), + EvalCAbiArg::TemporaryStackValue(EVAL_TEMP_CELL_OFFSET), + EvalCAbiArg::Symbol(&target_label), + EvalCAbiArg::Immediate(target_len as i64), + EvalCAbiArg::Immediate(i64::from(exclude_self)), + ], + ); abi::emit_jump(ctx.emitter, &done_label); ctx.emitter.label(&false_label); @@ -1659,36 +1593,24 @@ pub(super) fn lower_eval_class_constant_fetch( ) -> Result<()> { abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); ensure_eval_context(ctx)?; - load_eval_context_to_arg(ctx, 0); let (class_label, class_len) = ctx.data.add_string(class_name.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - &class_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - class_len as i64, - ); let (constant_label, constant_len) = ctx.data.add_string(constant_name.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - &constant_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - constant_len as i64, - ); - let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 5); - abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_class_constant_fetch"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::TemporaryStackValue(EVAL_CONTEXT_HANDLE_OFFSET), + EvalCAbiArg::Symbol(&class_label), + EvalCAbiArg::Immediate(class_len as i64), + EvalCAbiArg::Symbol(&constant_label), + EvalCAbiArg::Immediate(constant_len as i64), + EvalCAbiArg::TemporaryStackAddress(0), + ], + ); emit_eval_status_check(ctx); let result_reg = abi::int_result_reg(ctx.emitter); abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); @@ -1705,36 +1627,24 @@ pub(super) fn lower_eval_static_property_get( ) -> Result<()> { abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); ensure_eval_context(ctx)?; - load_eval_context_to_arg(ctx, 0); let (class_label, class_len) = ctx.data.add_string(class_name.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - &class_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - class_len as i64, - ); let (property_label, property_len) = ctx.data.add_string(property_name.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - &property_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - property_len as i64, - ); - let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 5); - abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_static_property_get"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::TemporaryStackValue(EVAL_CONTEXT_HANDLE_OFFSET), + EvalCAbiArg::Symbol(&class_label), + EvalCAbiArg::Immediate(class_len as i64), + EvalCAbiArg::Symbol(&property_label), + EvalCAbiArg::Immediate(property_len as i64), + EvalCAbiArg::TemporaryStackAddress(0), + ], + ); emit_eval_status_check(ctx); let result_reg = abi::int_result_reg(ctx.emitter); abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); @@ -1753,28 +1663,23 @@ pub(super) fn lower_eval_static_property_set( abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); store_eval_mixed_operand_at(ctx, value, EVAL_TEMP_CELL_OFFSET)?; ensure_eval_context(ctx)?; - load_eval_context_to_arg(ctx, 0); let target = format!("{}::{}", class_name, property_name); let (target_label, target_len) = ctx.data.add_string(target.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - &target_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - target_len as i64, - ); - let value_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); - abi::emit_load_temporary_stack_slot(ctx.emitter, value_arg, EVAL_TEMP_CELL_OFFSET); - let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); - abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_static_property_set"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::TemporaryStackValue(EVAL_CONTEXT_HANDLE_OFFSET), + EvalCAbiArg::Symbol(&target_label), + EvalCAbiArg::Immediate(target_len as i64), + EvalCAbiArg::TemporaryStackValue(EVAL_TEMP_CELL_OFFSET), + EvalCAbiArg::TemporaryStackAddress(0), + ], + ); emit_eval_status_check(ctx); abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); Ok(()) @@ -1892,25 +1797,20 @@ fn emit_eval_native_frame_override_probe( no_override_label: &str, ) { let (frame_label, frame_len) = ctx.data.add_string(frame_class.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 0), - &frame_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - frame_len as i64, - ); - let out_ptr_arg = abi::int_arg_reg_name(ctx.emitter.target, 2); - abi::emit_temporary_stack_address(ctx.emitter, out_ptr_arg, EVAL_CALLED_CLASS_PTR_OFFSET); - let out_len_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); - abi::emit_temporary_stack_address(ctx.emitter, out_len_arg, EVAL_CALLED_CLASS_LEN_OFFSET); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_native_frame_called_class_override"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::Symbol(&frame_label), + EvalCAbiArg::Immediate(frame_len as i64), + EvalCAbiArg::TemporaryStackAddress(EVAL_CALLED_CLASS_PTR_OFFSET), + EvalCAbiArg::TemporaryStackAddress(EVAL_CALLED_CLASS_LEN_OFFSET), + ], + ); abi::emit_branch_if_int_result_zero(ctx.emitter, no_override_label); } @@ -2045,8 +1945,11 @@ fn eval_class_relation_kind(name: &str) -> Result { fn emit_branch_if_eval_unboxed_not_object(ctx: &mut FunctionContext<'_>, label: &str) { match ctx.emitter.target.arch { Arch::AArch64 => { + let object_label = ctx.next_label("eval_unboxed_object_continue"); ctx.emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed value contains an object - ctx.emitter.instruction(&format!("b.ne {}", label)); // non-object values use the native false/empty fallback + ctx.emitter.instruction(&format!("b.eq {}", object_label)); // keep the short conditional branch within range for object values + ctx.emitter.instruction(&format!("b {}", label)); // take the long-range non-object fallback through an unconditional branch + ctx.emitter.label(&object_label); } Arch::X86_64 => { ctx.emitter.instruction("cmp rax, 6"); // runtime tag 6 means the Mixed value contains an object @@ -2240,20 +2143,17 @@ fn register_eval_declared_symbol_name( symbol_name: &str, name: &str, ) { - load_eval_context_local_to_arg(ctx, context_offset, 0); let (name_label, name_len) = ctx.data.add_string(name.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - &name_label, + let symbol = ctx.emitter.target.extern_symbol(symbol_name); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::FrameOffset(context_offset), + EvalCAbiArg::Symbol(&name_label), + EvalCAbiArg::Immediate(name_len as i64), + ], ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - name_len as i64, - ); - let symbol = ctx.emitter.target.extern_symbol(symbol_name); - abi::emit_call_label(ctx.emitter, &symbol); } /// Collects global PHP functions that can use the descriptor-invoker bridge. @@ -4010,38 +3910,23 @@ fn register_eval_native_function( ), Some(&invoker_label), ); - load_eval_context_local_to_arg(ctx, context_offset, 0); let (name_label, name_len) = ctx.data.add_string(registration.name.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - &name_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - name_len as i64, - ); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - &descriptor_label, - ); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - &invoker_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 5), - registration.signature.params.len() as i64, - ); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_register_native_function"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::FrameOffset(context_offset), + EvalCAbiArg::Symbol(&name_label), + EvalCAbiArg::Immediate(name_len as i64), + EvalCAbiArg::Symbol(&descriptor_label), + EvalCAbiArg::Symbol(&invoker_label), + EvalCAbiArg::Immediate(registration.signature.params.len() as i64), + ], + ); register_eval_native_function_bridge_support( ctx, context_offset, @@ -4113,6 +3998,70 @@ fn register_eval_native_function( Ok(()) } +/// Describes one scalar or pointer argument staged for an eval bridge C ABI call. +enum EvalCAbiArg<'a> { + FrameOffset(usize), + TemporaryStackValue(usize), + TemporaryStackAddress(usize), + Register(&'a str), + Symbol(&'a str), + SymbolOwned(String), + Immediate(i64), +} + +/// Materializes an eval bridge call through the target C ABI, including MS x64 +/// shadow space and stack arguments after the fourth positional slot. +fn emit_eval_c_abi_call(ctx: &mut FunctionContext<'_>, symbol: &str, args: &[EvalCAbiArg<'_>]) { + let value_reg = abi::int_result_reg(ctx.emitter); + for (index, arg) in args.iter().enumerate() { + let staged_bytes = index * 16; + match arg { + EvalCAbiArg::FrameOffset(offset) => { + abi::load_at_offset(ctx.emitter, value_reg, *offset); + } + EvalCAbiArg::TemporaryStackValue(offset) => { + abi::emit_load_temporary_stack_slot( + ctx.emitter, + value_reg, + offset + staged_bytes, + ); + } + EvalCAbiArg::TemporaryStackAddress(offset) => { + abi::emit_temporary_stack_address( + ctx.emitter, + value_reg, + offset + staged_bytes, + ); + } + EvalCAbiArg::Register(register) => { + abi::emit_push_reg(ctx.emitter, register); + continue; + } + EvalCAbiArg::Symbol(label) => { + abi::emit_symbol_address(ctx.emitter, value_reg, label); + } + EvalCAbiArg::SymbolOwned(label) => { + abi::emit_symbol_address(ctx.emitter, value_reg, label); + } + EvalCAbiArg::Immediate(value) => { + abi::emit_load_int_immediate(ctx.emitter, value_reg, *value); + } + } + abi::emit_push_reg(ctx.emitter, value_reg); + } + + let arg_types = vec![PhpType::Int; args.len()]; + let assignments = + abi::build_c_abi_outgoing_arg_assignments_for_target(ctx.emitter.target, &arg_types); + let overflow_bytes = abi::materialize_outgoing_args(ctx.emitter, &assignments); + abi::compact_windows_c_abi_stack_args(ctx.emitter, &assignments); + let call_pad_bytes = abi::outgoing_call_stack_pad_bytes(ctx.emitter.target, 0); + abi::emit_reserve_temporary_stack(ctx.emitter, call_pad_bytes); + abi::emit_call_label(ctx.emitter, symbol); + abi::emit_release_temporary_stack(ctx.emitter, call_pad_bytes); + abi::emit_release_temporary_stack(ctx.emitter, overflow_bytes); +} + /// Emits an eval-safe descriptor invoker for a registered native free function. fn emit_eval_native_function_invoker_inline( ctx: &mut FunctionContext<'_>, @@ -4142,24 +4091,8 @@ fn register_eval_native_method( context_offset: usize, registration: &EvalNativeMethodRegistration, ) { - load_eval_context_local_to_arg(ctx, context_offset, 0); let method_key = format!("{}::{}", registration.class_name, registration.method_name); let (method_key_label, method_key_len) = ctx.data.add_string(method_key.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - &method_key_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - method_key_len as i64, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - registration.signature.params.len() as i64, - ); let symbol = if registration.is_static { ctx.emitter .target @@ -4169,7 +4102,16 @@ fn register_eval_native_method( .target .extern_symbol("__elephc_eval_register_native_method") }; - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::FrameOffset(context_offset), + EvalCAbiArg::Symbol(&method_key_label), + EvalCAbiArg::Immediate(method_key_len as i64), + EvalCAbiArg::Immediate(registration.signature.params.len() as i64), + ], + ); register_eval_native_method_bridge_support( ctx, context_offset, @@ -4255,22 +4197,6 @@ fn register_eval_native_method_bridge_support( is_static: bool, bridge_supported: bool, ) { - load_eval_context_local_to_arg(ctx, context_offset, 0); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - method_key_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - method_key_len as i64, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - if bridge_supported { 1 } else { 0 }, - ); let symbol = if is_static { ctx.emitter .target @@ -4280,7 +4206,16 @@ fn register_eval_native_method_bridge_support( .target .extern_symbol("__elephc_eval_register_native_method_bridge_support") }; - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::FrameOffset(context_offset), + EvalCAbiArg::Symbol(method_key_label), + EvalCAbiArg::Immediate(method_key_len as i64), + EvalCAbiArg::Immediate(if bridge_supported { 1 } else { 0 }), + ], + ); } /// Emits one native method parameter-name registration call. @@ -4293,33 +4228,7 @@ fn register_eval_native_method_param( param_index: usize, param_name: &str, ) { - load_eval_context_local_to_arg(ctx, context_offset, 0); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - method_key_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - method_key_len as i64, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - param_index as i64, - ); let (param_name_label, param_name_len) = ctx.data.add_string(param_name.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - ¶m_name_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 5), - param_name_len as i64, - ); let symbol = if is_static { ctx.emitter .target @@ -4329,7 +4238,18 @@ fn register_eval_native_method_param( .target .extern_symbol("__elephc_eval_register_native_method_param") }; - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::FrameOffset(context_offset), + EvalCAbiArg::Symbol(method_key_label), + EvalCAbiArg::Immediate(method_key_len as i64), + EvalCAbiArg::Immediate(param_index as i64), + EvalCAbiArg::Symbol(¶m_name_label), + EvalCAbiArg::Immediate(param_name_len as i64), + ], + ); } /// Emits one native method parameter-flags registration call. @@ -4343,32 +4263,6 @@ fn register_eval_native_method_param_flags( is_by_ref: bool, is_variadic: bool, ) { - load_eval_context_local_to_arg(ctx, context_offset, 0); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - method_key_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - method_key_len as i64, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - param_index as i64, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - if is_by_ref { 1 } else { 0 }, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 5), - if is_variadic { 1 } else { 0 }, - ); let symbol = if is_static { ctx.emitter .target @@ -4378,7 +4272,18 @@ fn register_eval_native_method_param_flags( .target .extern_symbol("__elephc_eval_register_native_method_param_flags") }; - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::FrameOffset(context_offset), + EvalCAbiArg::Symbol(method_key_label), + EvalCAbiArg::Immediate(method_key_len as i64), + EvalCAbiArg::Immediate(param_index as i64), + EvalCAbiArg::Immediate(if is_by_ref { 1 } else { 0 }), + EvalCAbiArg::Immediate(if is_variadic { 1 } else { 0 }), + ], + ); } /// Emits one native method parameter-type registration call. @@ -4391,33 +4296,7 @@ fn register_eval_native_method_param_type( param_index: usize, type_spec: &str, ) { - load_eval_context_local_to_arg(ctx, context_offset, 0); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - method_key_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - method_key_len as i64, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - param_index as i64, - ); let (type_label, type_len) = ctx.data.add_string(type_spec.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - &type_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 5), - type_len as i64, - ); let symbol = if is_static { ctx.emitter .target @@ -4427,7 +4306,18 @@ fn register_eval_native_method_param_type( .target .extern_symbol("__elephc_eval_register_native_method_param_type") }; - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::FrameOffset(context_offset), + EvalCAbiArg::Symbol(method_key_label), + EvalCAbiArg::Immediate(method_key_len as i64), + EvalCAbiArg::Immediate(param_index as i64), + EvalCAbiArg::Symbol(&type_label), + EvalCAbiArg::Immediate(type_len as i64), + ], + ); } /// Emits one native method return-type registration call. @@ -4439,28 +4329,7 @@ fn register_eval_native_method_return_type( is_static: bool, type_spec: &str, ) { - load_eval_context_local_to_arg(ctx, context_offset, 0); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - method_key_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - method_key_len as i64, - ); let (type_label, type_len) = ctx.data.add_string(type_spec.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - &type_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - type_len as i64, - ); let symbol = if is_static { ctx.emitter .target @@ -4470,7 +4339,17 @@ fn register_eval_native_method_return_type( .target .extern_symbol("__elephc_eval_register_native_method_return_type") }; - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::FrameOffset(context_offset), + EvalCAbiArg::Symbol(method_key_label), + EvalCAbiArg::Immediate(method_key_len as i64), + EvalCAbiArg::Symbol(&type_label), + EvalCAbiArg::Immediate(type_len as i64), + ], + ); } /// Emits one native method parameter-default registration call. @@ -4483,35 +4362,9 @@ fn register_eval_native_method_param_default( param_index: usize, default: &EvalNativeCallableDefault, ) { - load_eval_context_local_to_arg(ctx, context_offset, 0); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - method_key_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - method_key_len as i64, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - param_index as i64, - ); - let symbol = match default { + let (symbol, value_1, value_2) = match default { EvalNativeCallableDefault::Scalar { kind, payload } => { - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - *kind, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 5), - *payload, - ); - if is_static { + let symbol = if is_static { ctx.emitter.target.extern_symbol( "__elephc_eval_register_native_static_method_param_default_scalar", ) @@ -4519,21 +4372,16 @@ fn register_eval_native_method_param_default( ctx.emitter .target .extern_symbol("__elephc_eval_register_native_method_param_default_scalar") - } + }; + ( + symbol, + EvalCAbiArg::Immediate(*kind), + EvalCAbiArg::Immediate(*payload), + ) } EvalNativeCallableDefault::String(value) => { let (default_label, default_len) = ctx.data.add_string(value.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - &default_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 5), - default_len as i64, - ); - if is_static { + let symbol = if is_static { ctx.emitter.target.extern_symbol( "__elephc_eval_register_native_static_method_param_default_string", ) @@ -4541,22 +4389,17 @@ fn register_eval_native_method_param_default( ctx.emitter .target .extern_symbol("__elephc_eval_register_native_method_param_default_string") - } + }; + ( + symbol, + EvalCAbiArg::SymbolOwned(default_label), + EvalCAbiArg::Immediate(default_len as i64), + ) } EvalNativeCallableDefault::Object { .. } => { let spec = encode_eval_native_object_default(default); let (default_label, default_len) = ctx.data.add_string(&spec); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - &default_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 5), - default_len as i64, - ); - if is_static { + let symbol = if is_static { ctx.emitter.target.extern_symbol( "__elephc_eval_register_native_static_method_param_default_object", ) @@ -4564,22 +4407,17 @@ fn register_eval_native_method_param_default( ctx.emitter .target .extern_symbol("__elephc_eval_register_native_method_param_default_object") - } + }; + ( + symbol, + EvalCAbiArg::SymbolOwned(default_label), + EvalCAbiArg::Immediate(default_len as i64), + ) } EvalNativeCallableDefault::Array(_) => { let spec = encode_eval_native_array_default(default); let (default_label, default_len) = ctx.data.add_string(&spec); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - &default_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 5), - default_len as i64, - ); - if is_static { + let symbol = if is_static { ctx.emitter.target.extern_symbol( "__elephc_eval_register_native_static_method_param_default_array", ) @@ -4587,10 +4425,26 @@ fn register_eval_native_method_param_default( ctx.emitter .target .extern_symbol("__elephc_eval_register_native_method_param_default_array") - } + }; + ( + symbol, + EvalCAbiArg::SymbolOwned(default_label), + EvalCAbiArg::Immediate(default_len as i64), + ) } }; - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::FrameOffset(context_offset), + EvalCAbiArg::Symbol(method_key_label), + EvalCAbiArg::Immediate(method_key_len as i64), + EvalCAbiArg::Immediate(param_index as i64), + value_1, + value_2, + ], + ); } /// Emits one native constructor signature registration call into the eval context. @@ -4599,29 +4453,22 @@ fn register_eval_native_constructor( context_offset: usize, registration: &EvalNativeConstructorRegistration, ) { - load_eval_context_local_to_arg(ctx, context_offset, 0); let (class_name_label, class_name_len) = ctx.data.add_string(registration.class_name.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - &class_name_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - class_name_len as i64, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - registration.signature.params.len() as i64, - ); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_register_native_constructor"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::FrameOffset(context_offset), + EvalCAbiArg::Symbol(&class_name_label), + EvalCAbiArg::Immediate(class_name_len as i64), + EvalCAbiArg::Immediate(registration.signature.params.len() as i64), + ], + ); register_eval_native_constructor_bridge_support( ctx, context_offset, @@ -4691,27 +4538,20 @@ fn register_eval_native_constructor_bridge_support( class_name_len: usize, bridge_supported: bool, ) { - load_eval_context_local_to_arg(ctx, context_offset, 0); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - class_name_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - class_name_len as i64, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - if bridge_supported { 1 } else { 0 }, - ); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_register_native_constructor_bridge_support"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::FrameOffset(context_offset), + EvalCAbiArg::Symbol(class_name_label), + EvalCAbiArg::Immediate(class_name_len as i64), + EvalCAbiArg::Immediate(if bridge_supported { 1 } else { 0 }), + ], + ); } /// Emits one native class-parent metadata registration call into the eval context. @@ -4721,34 +4561,23 @@ fn register_eval_native_class_parent( class_name: &str, parent_name: &str, ) { - load_eval_context_local_to_arg(ctx, context_offset, 0); let (class_name_label, class_name_len) = ctx.data.add_string(class_name.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - &class_name_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - class_name_len as i64, - ); let (parent_name_label, parent_name_len) = ctx.data.add_string(parent_name.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - &parent_name_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - parent_name_len as i64, - ); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_register_native_class_parent"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::FrameOffset(context_offset), + EvalCAbiArg::Symbol(&class_name_label), + EvalCAbiArg::Immediate(class_name_len as i64), + EvalCAbiArg::Symbol(&parent_name_label), + EvalCAbiArg::Immediate(parent_name_len as i64), + ], + ); } /// Emits one native property-type metadata registration call into the eval context. @@ -4757,38 +4586,27 @@ fn register_eval_native_property_type( context_offset: usize, registration: &EvalNativePropertyTypeRegistration, ) { - load_eval_context_local_to_arg(ctx, context_offset, 0); let property_key = format!( "{}::{}", registration.class_name, registration.property_name ); let (property_key_label, property_key_len) = ctx.data.add_string(property_key.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - &property_key_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - property_key_len as i64, - ); let (type_label, type_len) = ctx.data.add_string(registration.type_spec.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - &type_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - type_len as i64, - ); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_register_native_property_type"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::FrameOffset(context_offset), + EvalCAbiArg::Symbol(&property_key_label), + EvalCAbiArg::Immediate(property_key_len as i64), + EvalCAbiArg::Symbol(&type_label), + EvalCAbiArg::Immediate(type_len as i64), + ], + ); } /// Emits one native interface-property metadata registration call into the eval context. @@ -4797,7 +4615,6 @@ fn register_eval_native_interface_property( context_offset: usize, registration: &EvalNativeInterfacePropertyRegistration, ) { - load_eval_context_local_to_arg(ctx, context_offset, 0); let property_key = format!( "{}::{}::{}", registration.interface_name, @@ -4805,27 +4622,7 @@ fn register_eval_native_interface_property( registration.property_name ); let (property_key_label, property_key_len) = ctx.data.add_string(property_key.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - &property_key_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - property_key_len as i64, - ); let (type_label, type_len) = ctx.data.add_string(registration.type_spec.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - &type_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - type_len as i64, - ); let mut flags = 0; if registration.requires_get { flags |= NATIVE_PROPERTY_REQUIRES_GET; @@ -4833,16 +4630,22 @@ fn register_eval_native_interface_property( if registration.requires_set { flags |= NATIVE_PROPERTY_REQUIRES_SET; } - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 5), - flags, - ); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_register_native_interface_property"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::FrameOffset(context_offset), + EvalCAbiArg::Symbol(&property_key_label), + EvalCAbiArg::Immediate(property_key_len as i64), + EvalCAbiArg::Symbol(&type_label), + EvalCAbiArg::Immediate(type_len as i64), + EvalCAbiArg::Immediate(flags), + ], + ); } /// Emits one native abstract-property metadata registration call into the eval context. @@ -4851,33 +4654,12 @@ fn register_eval_native_abstract_property( context_offset: usize, registration: &EvalNativeAbstractPropertyRegistration, ) { - load_eval_context_local_to_arg(ctx, context_offset, 0); let property_key = format!( "{}::{}::{}", registration.class_name, registration.declaring_class_name, registration.property_name ); let (property_key_label, property_key_len) = ctx.data.add_string(property_key.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - &property_key_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - property_key_len as i64, - ); let (type_label, type_len) = ctx.data.add_string(registration.type_spec.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - &type_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - type_len as i64, - ); let mut flags = 0; if registration.requires_get { flags |= NATIVE_PROPERTY_REQUIRES_GET; @@ -4885,16 +4667,22 @@ fn register_eval_native_abstract_property( if registration.requires_set { flags |= NATIVE_PROPERTY_REQUIRES_SET; } - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 5), - flags, - ); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_register_native_abstract_property"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::FrameOffset(context_offset), + EvalCAbiArg::Symbol(&property_key_label), + EvalCAbiArg::Immediate(property_key_len as i64), + EvalCAbiArg::Symbol(&type_label), + EvalCAbiArg::Immediate(type_len as i64), + EvalCAbiArg::Immediate(flags), + ], + ); } /// Emits one native property-default metadata registration call into the eval context. @@ -4909,68 +4697,50 @@ fn register_eval_native_property_default( registration.class_name, registration.property_name ); let (property_key_label, property_key_len) = ctx.data.add_string(property_key.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - &property_key_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - property_key_len as i64, - ); - let symbol = match ®istration.default { + let (symbol, value_1, value_2) = match ®istration.default { EvalNativeCallableDefault::Scalar { kind, payload } => { - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - *kind, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - *payload, - ); - ctx.emitter - .target - .extern_symbol("__elephc_eval_register_native_property_default_scalar") + ( + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_property_default_scalar"), + EvalCAbiArg::Immediate(*kind), + EvalCAbiArg::Immediate(*payload), + ) } EvalNativeCallableDefault::String(value) => { let (default_label, default_len) = ctx.data.add_string(value.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - &default_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - default_len as i64, - ); - ctx.emitter - .target - .extern_symbol("__elephc_eval_register_native_property_default_string") + ( + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_property_default_string"), + EvalCAbiArg::SymbolOwned(default_label), + EvalCAbiArg::Immediate(default_len as i64), + ) } EvalNativeCallableDefault::Array(_) => { let spec = encode_eval_native_array_default(®istration.default); let (default_label, default_len) = ctx.data.add_string(&spec); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - &default_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - default_len as i64, - ); - ctx.emitter - .target - .extern_symbol("__elephc_eval_register_native_property_default_array") + ( + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_property_default_array"), + EvalCAbiArg::SymbolOwned(default_label), + EvalCAbiArg::Immediate(default_len as i64), + ) } EvalNativeCallableDefault::Object { .. } => return, }; - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::FrameOffset(context_offset), + EvalCAbiArg::Symbol(&property_key_label), + EvalCAbiArg::Immediate(property_key_len as i64), + value_1, + value_2, + ], + ); } /// Emits one native member-attribute metadata registration call into the eval context. @@ -4979,24 +4749,21 @@ fn register_eval_native_member_attribute( context_offset: usize, registration: &EvalNativeMemberAttributeRegistration, ) { - load_eval_context_local_to_arg(ctx, context_offset, 0); let record = eval_native_member_attribute_record(registration); let (record_label, record_len) = ctx.data.add_string(&record); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - &record_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - record_len as i64, - ); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_register_native_member_attribute"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::FrameOffset(context_offset), + EvalCAbiArg::Symbol(&record_label), + EvalCAbiArg::Immediate(record_len as i64), + ], + ); } /// Encodes one member-attribute registration record for the eval bridge ABI. @@ -5110,38 +4877,23 @@ fn register_eval_native_constructor_param( param_index: usize, param_name: &str, ) { - load_eval_context_local_to_arg(ctx, context_offset, 0); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - class_name_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - class_name_len as i64, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - param_index as i64, - ); let (param_name_label, param_name_len) = ctx.data.add_string(param_name.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - ¶m_name_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 5), - param_name_len as i64, - ); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_register_native_constructor_param"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::FrameOffset(context_offset), + EvalCAbiArg::Symbol(class_name_label), + EvalCAbiArg::Immediate(class_name_len as i64), + EvalCAbiArg::Immediate(param_index as i64), + EvalCAbiArg::Symbol(¶m_name_label), + EvalCAbiArg::Immediate(param_name_len as i64), + ], + ); } /// Emits one native constructor parameter-flags registration call. @@ -5154,37 +4906,22 @@ fn register_eval_native_constructor_param_flags( is_by_ref: bool, is_variadic: bool, ) { - load_eval_context_local_to_arg(ctx, context_offset, 0); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - class_name_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - class_name_len as i64, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - param_index as i64, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - if is_by_ref { 1 } else { 0 }, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 5), - if is_variadic { 1 } else { 0 }, - ); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_register_native_constructor_param_flags"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::FrameOffset(context_offset), + EvalCAbiArg::Symbol(class_name_label), + EvalCAbiArg::Immediate(class_name_len as i64), + EvalCAbiArg::Immediate(param_index as i64), + EvalCAbiArg::Immediate(if is_by_ref { 1 } else { 0 }), + EvalCAbiArg::Immediate(if is_variadic { 1 } else { 0 }), + ], + ); } /// Emits one native constructor parameter-type registration call. @@ -5196,38 +4933,23 @@ fn register_eval_native_constructor_param_type( param_index: usize, type_spec: &str, ) { - load_eval_context_local_to_arg(ctx, context_offset, 0); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - class_name_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - class_name_len as i64, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - param_index as i64, - ); let (type_label, type_len) = ctx.data.add_string(type_spec.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - &type_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 5), - type_len as i64, - ); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_register_native_constructor_param_type"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::FrameOffset(context_offset), + EvalCAbiArg::Symbol(class_name_label), + EvalCAbiArg::Immediate(class_name_len as i64), + EvalCAbiArg::Immediate(param_index as i64), + EvalCAbiArg::Symbol(&type_label), + EvalCAbiArg::Immediate(type_len as i64), + ], + ); } /// Emits one native constructor parameter-default registration call. @@ -5239,90 +4961,61 @@ fn register_eval_native_constructor_param_default( param_index: usize, default: &EvalNativeCallableDefault, ) { - load_eval_context_local_to_arg(ctx, context_offset, 0); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - class_name_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - class_name_len as i64, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - param_index as i64, - ); - let symbol = match default { + let (symbol, value_1, value_2) = match default { EvalNativeCallableDefault::Scalar { kind, payload } => { - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - *kind, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 5), - *payload, - ); - ctx.emitter - .target - .extern_symbol("__elephc_eval_register_native_constructor_param_default_scalar") + ( + ctx.emitter.target.extern_symbol( + "__elephc_eval_register_native_constructor_param_default_scalar", + ), + EvalCAbiArg::Immediate(*kind), + EvalCAbiArg::Immediate(*payload), + ) } EvalNativeCallableDefault::String(value) => { let (default_label, default_len) = ctx.data.add_string(value.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - &default_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 5), - default_len as i64, - ); - ctx.emitter - .target - .extern_symbol("__elephc_eval_register_native_constructor_param_default_string") + ( + ctx.emitter.target.extern_symbol( + "__elephc_eval_register_native_constructor_param_default_string", + ), + EvalCAbiArg::SymbolOwned(default_label), + EvalCAbiArg::Immediate(default_len as i64), + ) } EvalNativeCallableDefault::Object { .. } => { let spec = encode_eval_native_object_default(default); let (default_label, default_len) = ctx.data.add_string(&spec); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - &default_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 5), - default_len as i64, - ); - ctx.emitter - .target - .extern_symbol("__elephc_eval_register_native_constructor_param_default_object") + ( + ctx.emitter.target.extern_symbol( + "__elephc_eval_register_native_constructor_param_default_object", + ), + EvalCAbiArg::SymbolOwned(default_label), + EvalCAbiArg::Immediate(default_len as i64), + ) } EvalNativeCallableDefault::Array(_) => { let spec = encode_eval_native_array_default(default); let (default_label, default_len) = ctx.data.add_string(&spec); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - &default_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 5), - default_len as i64, - ); - ctx.emitter - .target - .extern_symbol("__elephc_eval_register_native_constructor_param_default_array") + ( + ctx.emitter.target.extern_symbol( + "__elephc_eval_register_native_constructor_param_default_array", + ), + EvalCAbiArg::SymbolOwned(default_label), + EvalCAbiArg::Immediate(default_len as i64), + ) } }; - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::FrameOffset(context_offset), + EvalCAbiArg::Symbol(class_name_label), + EvalCAbiArg::Immediate(class_name_len as i64), + EvalCAbiArg::Immediate(param_index as i64), + value_1, + value_2, + ], + ); } /// Emits one native-function parameter-name registration call. @@ -5334,38 +5027,23 @@ fn register_eval_native_function_param( param_index: usize, param_name: &str, ) { - load_eval_context_local_to_arg(ctx, context_offset, 0); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - function_name_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - function_name_len as i64, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - param_index as i64, - ); let (param_name_label, param_name_len) = ctx.data.add_string(param_name.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - ¶m_name_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 5), - param_name_len as i64, - ); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_register_native_function_param"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::FrameOffset(context_offset), + EvalCAbiArg::Symbol(function_name_label), + EvalCAbiArg::Immediate(function_name_len as i64), + EvalCAbiArg::Immediate(param_index as i64), + EvalCAbiArg::Symbol(¶m_name_label), + EvalCAbiArg::Immediate(param_name_len as i64), + ], + ); } /// Emits one native-function bridge-support registration call. @@ -5376,27 +5054,20 @@ fn register_eval_native_function_bridge_support( function_name_len: usize, bridge_supported: bool, ) { - load_eval_context_local_to_arg(ctx, context_offset, 0); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - function_name_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - function_name_len as i64, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - if bridge_supported { 1 } else { 0 }, - ); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_register_native_function_bridge_support"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::FrameOffset(context_offset), + EvalCAbiArg::Symbol(function_name_label), + EvalCAbiArg::Immediate(function_name_len as i64), + EvalCAbiArg::Immediate(if bridge_supported { 1 } else { 0 }), + ], + ); } /// Emits one native-function parameter-flags registration call. @@ -5409,37 +5080,22 @@ fn register_eval_native_function_param_flags( is_by_ref: bool, is_variadic: bool, ) { - load_eval_context_local_to_arg(ctx, context_offset, 0); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - function_name_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - function_name_len as i64, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - param_index as i64, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - if is_by_ref { 1 } else { 0 }, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 5), - if is_variadic { 1 } else { 0 }, - ); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_register_native_function_param_flags"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::FrameOffset(context_offset), + EvalCAbiArg::Symbol(function_name_label), + EvalCAbiArg::Immediate(function_name_len as i64), + EvalCAbiArg::Immediate(param_index as i64), + EvalCAbiArg::Immediate(if is_by_ref { 1 } else { 0 }), + EvalCAbiArg::Immediate(if is_variadic { 1 } else { 0 }), + ], + ); } /// Emits one native-function parameter-type registration call. @@ -5451,38 +5107,23 @@ fn register_eval_native_function_param_type( param_index: usize, type_spec: &str, ) { - load_eval_context_local_to_arg(ctx, context_offset, 0); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - function_name_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - function_name_len as i64, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - param_index as i64, - ); let (type_label, type_len) = ctx.data.add_string(type_spec.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - &type_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 5), - type_len as i64, - ); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_register_native_function_param_type"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::FrameOffset(context_offset), + EvalCAbiArg::Symbol(function_name_label), + EvalCAbiArg::Immediate(function_name_len as i64), + EvalCAbiArg::Immediate(param_index as i64), + EvalCAbiArg::Symbol(&type_label), + EvalCAbiArg::Immediate(type_len as i64), + ], + ); } /// Emits one native-function return-type registration call. @@ -5493,33 +5134,22 @@ fn register_eval_native_function_return_type( function_name_len: usize, type_spec: &str, ) { - load_eval_context_local_to_arg(ctx, context_offset, 0); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - function_name_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - function_name_len as i64, - ); let (type_label, type_len) = ctx.data.add_string(type_spec.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - &type_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - type_len as i64, - ); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_register_native_function_return_type"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::FrameOffset(context_offset), + EvalCAbiArg::Symbol(function_name_label), + EvalCAbiArg::Immediate(function_name_len as i64), + EvalCAbiArg::Symbol(&type_label), + EvalCAbiArg::Immediate(type_len as i64), + ], + ); } /// Emits one native function parameter-default registration call. @@ -5531,90 +5161,61 @@ fn register_eval_native_function_param_default( param_index: usize, default: &EvalNativeCallableDefault, ) { - load_eval_context_local_to_arg(ctx, context_offset, 0); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - function_name_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - function_name_len as i64, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - param_index as i64, - ); - let symbol = match default { + let (symbol, value_1, value_2) = match default { EvalNativeCallableDefault::Scalar { kind, payload } => { - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - *kind, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 5), - *payload, - ); - ctx.emitter - .target - .extern_symbol("__elephc_eval_register_native_function_param_default_scalar") + ( + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_function_param_default_scalar"), + EvalCAbiArg::Immediate(*kind), + EvalCAbiArg::Immediate(*payload), + ) } EvalNativeCallableDefault::String(value) => { let (default_label, default_len) = ctx.data.add_string(value.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - &default_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 5), - default_len as i64, - ); - ctx.emitter - .target - .extern_symbol("__elephc_eval_register_native_function_param_default_string") + ( + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_function_param_default_string"), + EvalCAbiArg::SymbolOwned(default_label), + EvalCAbiArg::Immediate(default_len as i64), + ) } EvalNativeCallableDefault::Object { .. } => { let spec = encode_eval_native_object_default(default); let (default_label, default_len) = ctx.data.add_string(&spec); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - &default_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 5), - default_len as i64, - ); - ctx.emitter - .target - .extern_symbol("__elephc_eval_register_native_function_param_default_object") + ( + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_function_param_default_object"), + EvalCAbiArg::SymbolOwned(default_label), + EvalCAbiArg::Immediate(default_len as i64), + ) } EvalNativeCallableDefault::Array(_) => { let spec = encode_eval_native_array_default(default); let (default_label, default_len) = ctx.data.add_string(&spec); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - &default_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 5), - default_len as i64, - ); - ctx.emitter - .target - .extern_symbol("__elephc_eval_register_native_function_param_default_array") + ( + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_function_param_default_array"), + EvalCAbiArg::SymbolOwned(default_label), + EvalCAbiArg::Immediate(default_len as i64), + ) } }; - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::FrameOffset(context_offset), + EvalCAbiArg::Symbol(function_name_label), + EvalCAbiArg::Immediate(function_name_len as i64), + EvalCAbiArg::Immediate(param_index as i64), + value_1, + value_2, + ], + ); } /// Loads the persistent eval context local into the selected integer argument register. @@ -5633,14 +5234,6 @@ fn load_eval_context_to_arg(ctx: &mut FunctionContext<'_>, arg_index: usize) { abi::emit_load_temporary_stack_slot(ctx.emitter, arg_reg, EVAL_CONTEXT_HANDLE_OFFSET); } -/// Reloads the saved eval source string into the bridge code pointer/length arguments. -fn move_saved_eval_code_to_eval_args(ctx: &mut FunctionContext<'_>) { - let code_ptr_arg = abi::int_arg_reg_name(ctx.emitter.target, 2); - let code_len_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); - abi::emit_load_temporary_stack_slot(ctx.emitter, code_ptr_arg, EVAL_CODE_PTR_OFFSET); - abi::emit_load_temporary_stack_slot(ctx.emitter, code_len_arg, EVAL_CODE_LEN_OFFSET); -} - /// Ensures a persistent eval scope exists and stores its handle in the scratch frame. fn ensure_eval_scope(ctx: &mut FunctionContext<'_>) -> Result<()> { let slot = eval_scope_slot(ctx)?; @@ -5728,33 +5321,22 @@ fn push_eval_context_class_scope(ctx: &mut FunctionContext<'_>) -> Result let (called_ptr_reg, called_len_reg) = abi::string_result_regs(ctx.emitter); abi::emit_store_to_sp(ctx.emitter, called_ptr_reg, EVAL_CALLED_CLASS_PTR_OFFSET); abi::emit_store_to_sp(ctx.emitter, called_len_reg, EVAL_CALLED_CLASS_LEN_OFFSET); - load_eval_context_to_arg(ctx, 0); let (class_label, class_len) = ctx.data.add_string(class_name.as_bytes()); - abi::emit_symbol_address( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), - &class_label, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - class_len as i64, - ); - abi::emit_load_temporary_stack_slot( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - EVAL_CALLED_CLASS_PTR_OFFSET, - ); - abi::emit_load_temporary_stack_slot( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - EVAL_CALLED_CLASS_LEN_OFFSET, - ); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_context_push_class_scope"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::TemporaryStackValue(EVAL_CONTEXT_HANDLE_OFFSET), + EvalCAbiArg::Symbol(&class_label), + EvalCAbiArg::Immediate(class_len as i64), + EvalCAbiArg::TemporaryStackValue(EVAL_CALLED_CLASS_PTR_OFFSET), + EvalCAbiArg::TemporaryStackValue(EVAL_CALLED_CLASS_LEN_OFFSET), + ], + ); emit_eval_status_check(ctx); Ok(true) } @@ -6161,52 +5743,59 @@ fn scope_set_flags_for_type(ty: &PhpType) -> i64 { /// Calls `__elephc_eval_scope_set` for a boxed value identified by a static name. fn emit_eval_scope_set_name(ctx: &mut FunctionContext<'_>, name: &str, flags: i64) { let (name_label, name_len) = ctx.data.add_string(name.as_bytes()); - load_eval_scope_to_arg(ctx, 0); - let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); - abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - name_len as i64, - ); - abi::emit_load_temporary_stack_slot( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - EVAL_TEMP_CELL_OFFSET, + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_set"); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::TemporaryStackValue(EVAL_SCOPE_HANDLE_OFFSET), + EvalCAbiArg::Symbol(&name_label), + EvalCAbiArg::Immediate(name_len as i64), + EvalCAbiArg::TemporaryStackValue(EVAL_TEMP_CELL_OFFSET), + EvalCAbiArg::Immediate(flags), + ], ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - flags, + emit_eval_status_check(ctx); +} + +/// Calls `__elephc_eval_scope_get` for a static name and caller-provided scratch slots. +fn emit_eval_scope_get_name( + ctx: &mut FunctionContext<'_>, + name: &str, + out_cell_offset: usize, + out_flags_offset: usize, +) { + let (name_label, name_len) = ctx.data.add_string(name.as_bytes()); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_get"); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::TemporaryStackValue(EVAL_SCOPE_HANDLE_OFFSET), + EvalCAbiArg::Symbol(&name_label), + EvalCAbiArg::Immediate(name_len as i64), + EvalCAbiArg::TemporaryStackAddress(out_cell_offset), + EvalCAbiArg::TemporaryStackAddress(out_flags_offset), + ], ); - let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_set"); - abi::emit_call_label(ctx.emitter, &symbol); emit_eval_status_check(ctx); } /// Calls `__elephc_eval_scope_set` for one boxed global value. fn emit_eval_global_scope_set(ctx: &mut FunctionContext<'_>, global: &EvalSyncGlobal, flags: i64) { let (name_label, name_len) = ctx.data.add_string(global.name.as_bytes()); - load_eval_global_scope_to_arg(ctx, 0); - let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); - abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - name_len as i64, - ); - abi::emit_load_temporary_stack_slot( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - EVAL_TEMP_CELL_OFFSET, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - flags, - ); let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_set"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::TemporaryStackValue(EVAL_GLOBAL_SCOPE_HANDLE_OFFSET), + EvalCAbiArg::Symbol(&name_label), + EvalCAbiArg::Immediate(name_len as i64), + EvalCAbiArg::TemporaryStackValue(EVAL_TEMP_CELL_OFFSET), + EvalCAbiArg::Immediate(flags), + ], + ); emit_eval_status_check(ctx); } @@ -6216,26 +5805,21 @@ fn mark_eval_scope_global_aliases(ctx: &mut FunctionContext<'_>, aliases: &[Eval let (name_label, name_len) = ctx.data.add_string(alias.name.as_bytes()); let (global_name_label, global_name_len) = ctx.data.add_string(alias.global_name.as_bytes()); - load_eval_scope_to_arg(ctx, 0); - let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); - abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - name_len as i64, - ); - let global_name_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); - abi::emit_symbol_address(ctx.emitter, global_name_arg, &global_name_label); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - global_name_len as i64, - ); let symbol = ctx .emitter .target .extern_symbol("__elephc_eval_scope_mark_global_alias"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::TemporaryStackValue(EVAL_SCOPE_HANDLE_OFFSET), + EvalCAbiArg::Symbol(&name_label), + EvalCAbiArg::Immediate(name_len as i64), + EvalCAbiArg::Symbol(&global_name_label), + EvalCAbiArg::Immediate(global_name_len as i64), + ], + ); emit_eval_status_check(ctx); } } @@ -6243,26 +5827,18 @@ fn mark_eval_scope_global_aliases(ctx: &mut FunctionContext<'_>, aliases: &[Eval /// Calls `__elephc_eval_scope_set` for one boxed local value. fn emit_eval_scope_set(ctx: &mut FunctionContext<'_>, local: &EvalSyncLocal, flags: i64) { let (name_label, name_len) = ctx.data.add_string(local.name.as_bytes()); - load_eval_scope_to_arg(ctx, 0); - let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); - abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - name_len as i64, - ); - abi::emit_load_temporary_stack_slot( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 3), - EVAL_TEMP_CELL_OFFSET, - ); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 4), - flags, - ); let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_set"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::TemporaryStackValue(EVAL_SCOPE_HANDLE_OFFSET), + EvalCAbiArg::Symbol(&name_label), + EvalCAbiArg::Immediate(name_len as i64), + EvalCAbiArg::TemporaryStackValue(EVAL_TEMP_CELL_OFFSET), + EvalCAbiArg::Immediate(flags), + ], + ); emit_eval_status_check(ctx); } @@ -6326,68 +5902,39 @@ fn reload_eval_globals_from_local_scope( Ok(()) } -/// Calls `__elephc_eval_scope_get` for a static name and caller-provided scratch slots. -fn emit_eval_scope_get_name( - ctx: &mut FunctionContext<'_>, - name: &str, - out_cell_offset: usize, - out_flags_offset: usize, -) { - let (name_label, name_len) = ctx.data.add_string(name.as_bytes()); - load_eval_scope_to_arg(ctx, 0); - let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); - abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - name_len as i64, - ); - let out_cell_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); - abi::emit_temporary_stack_address(ctx.emitter, out_cell_arg, out_cell_offset); - let out_flags_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); - abi::emit_temporary_stack_address(ctx.emitter, out_flags_arg, out_flags_offset); - let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_get"); - abi::emit_call_label(ctx.emitter, &symbol); - emit_eval_status_check(ctx); -} - /// Calls `__elephc_eval_scope_get` and stores out cell/flags at the start of eval scratch. fn emit_eval_scope_get(ctx: &mut FunctionContext<'_>, local: &EvalSyncLocal) { let (name_label, name_len) = ctx.data.add_string(local.name.as_bytes()); - load_eval_scope_to_arg(ctx, 0); - let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); - abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - name_len as i64, - ); - let out_cell_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); - abi::emit_temporary_stack_address(ctx.emitter, out_cell_arg, 0); - let out_flags_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); - abi::emit_temporary_stack_address(ctx.emitter, out_flags_arg, 8); let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_get"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::TemporaryStackValue(EVAL_SCOPE_HANDLE_OFFSET), + EvalCAbiArg::Symbol(&name_label), + EvalCAbiArg::Immediate(name_len as i64), + EvalCAbiArg::TemporaryStackAddress(0), + EvalCAbiArg::TemporaryStackAddress(8), + ], + ); emit_eval_status_check(ctx); } /// Calls `__elephc_eval_scope_get` for one program global. fn emit_eval_global_scope_get(ctx: &mut FunctionContext<'_>, global: &EvalSyncGlobal) { let (name_label, name_len) = ctx.data.add_string(global.name.as_bytes()); - load_eval_global_scope_to_arg(ctx, 0); - let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); - abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), - name_len as i64, - ); - let out_cell_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); - abi::emit_temporary_stack_address(ctx.emitter, out_cell_arg, 0); - let out_flags_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); - abi::emit_temporary_stack_address(ctx.emitter, out_flags_arg, 8); let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_get"); - abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_c_abi_call( + ctx, + &symbol, + &[ + EvalCAbiArg::TemporaryStackValue(EVAL_GLOBAL_SCOPE_HANDLE_OFFSET), + EvalCAbiArg::Symbol(&name_label), + EvalCAbiArg::Immediate(name_len as i64), + EvalCAbiArg::TemporaryStackAddress(0), + EvalCAbiArg::TemporaryStackAddress(8), + ], + ); emit_eval_status_check(ctx); } diff --git a/src/codegen/lower_inst/builtins/io.rs b/src/codegen/lower_inst/builtins/io.rs index 97e43b50cc..e1661e31a2 100644 --- a/src/codegen/lower_inst/builtins/io.rs +++ b/src/codegen/lower_inst/builtins/io.rs @@ -10,7 +10,7 @@ //! string result registers expected by the shared runtime helpers. use crate::codegen::{abi, callable_descriptor, emit_box_current_value_as_mixed, NULL_SENTINEL}; -use crate::codegen::platform::Arch; +use crate::codegen::platform::{Arch, Platform}; use crate::codegen::{CodegenIrError, Result}; use crate::ir::{Immediate, Instruction, LocalSlotId, Op, ValueDef, ValueId}; use crate::types::PhpType; @@ -64,16 +64,14 @@ pub(crate) fn lower_file_get_contents( store_if_result(ctx, inst) } -/// Publishes bridge/decompressor entry points into runtime slots used by -/// dynamic `phar://` reads. +/// Publishes the pure-Rust bridge entry point used by dynamic `phar://` reads. +/// +/// `elephc_phar_extract_url` handles native, tar, ZIP, gzip, and bzip2 itself; +/// publishing system decompressor slots here would add unnecessary `-lz` and +/// `-lbz2` dependencies to every dynamic filesystem path. fn publish_dynamic_phar_function_pointers(ctx: &mut FunctionContext<'_>) { - const ENTRIES: &[(&str, &str)] = &[ - ("elephc_phar_extract_url", "_elephc_phar_extract_url_fn"), - ("inflateInit2_", "_phar_zlib_inflate_init2_fn"), - ("inflate", "_phar_zlib_inflate_fn"), - ("inflateEnd", "_phar_zlib_inflate_end_fn"), - ("BZ2_bzBuffToBuffDecompress", "_phar_bz2_decompress_fn"), - ]; + const ENTRIES: &[(&str, &str)] = + &[("elephc_phar_extract_url", "_elephc_phar_extract_url_fn")]; match ctx.emitter.target.arch { Arch::AArch64 => { for (c_name, slot) in ENTRIES { @@ -87,7 +85,7 @@ fn publish_dynamic_phar_function_pointers(ctx: &mut FunctionContext<'_>) { for (c_name, slot) in ENTRIES { let extern_sym = ctx.emitter.target.extern_symbol(c_name); abi::emit_extern_symbol_address(ctx.emitter, "r9", &extern_sym); - abi::emit_store_reg_to_symbol(ctx.emitter, "r9", slot, 0); // publish the decompressor entry into its runtime slot + abi::emit_store_reg_to_symbol(ctx.emitter, "r9", slot, 0); // publish the decompressor entry into its runtime slot } } } @@ -108,7 +106,7 @@ fn publish_phar_bridge_entries(ctx: &mut FunctionContext<'_>, entries: &[(&str, for (c_name, slot) in entries { let extern_sym = ctx.emitter.target.extern_symbol(c_name); abi::emit_extern_symbol_address(ctx.emitter, "r9", &extern_sym); - abi::emit_store_reg_to_symbol(ctx.emitter, "r9", slot, 0); // publish the PHAR bridge entry into its runtime slot + abi::emit_store_reg_to_symbol(ctx.emitter, "r9", slot, 0); // publish the PHAR bridge entry into its runtime slot } } } @@ -534,6 +532,13 @@ fn emit_php_filter_table_stamps(ctx: &mut FunctionContext<'_>, mode_bits: u8, fi ctx.emitter.instruction("cmp r9, 9"); // test whether fopen returned a resource ctx.emitter.instruction(&format!("jne {}", done_label)); // leave false results unmodified ctx.emitter.instruction("mov rcx, QWORD PTR [rax + 8]"); // load the descriptor payload from the boxed resource + if ctx.emitter.target.platform == Platform::Windows { + abi::emit_push_reg(ctx.emitter, "rax"); + ctx.emitter.instruction("mov rdi, rcx"); // pass the opaque descriptor to the bounded stream-state registry + abi::emit_call_label(ctx.emitter, "__rt_win_stream_slot"); + ctx.emitter.instruction("mov rcx, rax"); // index filter tables through the compact Windows slot + abi::emit_pop_reg(ctx.emitter, "rax"); + } if mode_bits & 1 != 0 { abi::emit_symbol_address(ctx.emitter, "r8", "_stream_read_filters"); // read-filter table base ctx.emitter.instruction(&format!("mov BYTE PTR [r8 + rcx], {}", filter_id)); // attach the read filter to this descriptor @@ -1295,6 +1300,50 @@ pub(crate) fn lower_stream_context_get_params( store_if_result(ctx, inst) } +/// Boxes a read-all result as `Mixed`, then reclaims the block the read may have owned. +/// +/// `__rt_stream_get_contents` hands back either a borrowed slice of the 64 KiB concat arena, +/// when the stream fitted in it, or an owned `__rt_heap_alloc` block when it did not. The +/// boxing helper does not adopt either one: `__rt_mixed_from_value` tag 1 goes through +/// `__rt_str_persist`, which always allocates and byte-copies, so the source is dead the +/// moment boxing returns. Without this the owned case stranded one block per call — one +/// 524288-byte block per iteration on a 500 KB stream. +/// +/// `__rt_heap_free_safe` separates the two shapes exactly rather than heuristically: +/// `_concat_buf` and `_heap_buf` are distinct `.comm` objects, so an arena pointer can never +/// satisfy its managed-heap window test and passes through untouched. +/// +/// Only correct because no EIR value names the pre-box pointer: the builtin's checker +/// signature is the union `string|false`, so `lower_release` always takes the +/// `__rt_decref_mixed` arm and never frees this block a second time. Do not reuse this shape +/// for a builtin typed plain `string` — `fread()` is exactly that, and there the EIR release +/// already reclaims the block, so a second free here would be a double free. +fn box_read_all_result_and_release_source(ctx: &mut FunctionContext<'_>) { + let (ptr_reg, _) = abi::string_result_regs(ctx.emitter); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, ptr_reg); // preserve the pre-box source pointer, which boxing overwrites + crate::codegen::emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Str); + abi::emit_push_reg(ctx.emitter, result_reg); // preserve the boxed mixed cell across the source release + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("ldr x0, [sp, #16]"); // reload the source pointer saved below the boxed result + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rax, QWORD PTR [rsp + 16]"); // reload the source pointer saved below the boxed result + } + } + abi::emit_call_label(ctx.emitter, "__rt_heap_free_safe"); + abi::emit_pop_reg(ctx.emitter, result_reg); // restore the boxed mixed cell as the current result + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("add sp, sp, #16"); // discard the saved source-pointer slot + } + Arch::X86_64 => { + ctx.emitter.instruction("add rsp, 16"); // discard the saved source-pointer slot + } + } +} + /// Lowers `stream_get_contents(stream, length?, offset?)` to `string|false`. pub(crate) fn lower_stream_get_contents( ctx: &mut FunctionContext<'_>, @@ -1305,7 +1354,7 @@ pub(crate) fn lower_stream_get_contents( load_stream_fd_to_result(ctx, stream, "stream_get_contents")?; if inst.operands.len() == 1 { lower_stream_get_contents_read_all(ctx); - crate::codegen::emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Str); + box_read_all_result_and_release_source(ctx); return store_if_result(ctx, inst); } @@ -1337,7 +1386,7 @@ pub(crate) fn lower_stream_get_contents( ctx.emitter.label(&read_all); lower_stream_get_contents_reload_fd_and_leave_frame(ctx); lower_stream_get_contents_read_all(ctx); - crate::codegen::emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Str); + box_read_all_result_and_release_source(ctx); match ctx.emitter.target.arch { Arch::AArch64 => { ctx.emitter.instruction(&format!("b {}", done)); // skip the seek-failure false result after reading successfully @@ -1477,13 +1526,17 @@ pub(crate) fn lower_stream_get_transports( inst: &Instruction, ) -> Result<()> { super::ensure_arg_count(inst, "stream_get_transports", 0)?; - emit_static_string_array( - ctx, + let transports: &[&str] = if ctx.emitter.target.platform == Platform::Windows { &[ - "tcp", "udp", "unix", "udg", "tls", "ssl", "sslv2", "sslv3", - "tlsv1.0", "tlsv1.1", "tlsv1.2", "tlsv1.3", - ], - ); + "tcp", "udp", "tls", "ssl", "tlsv1.0", "tlsv1.1", "tlsv1.2", "tlsv1.3", + ] + } else { + &[ + "tcp", "udp", "unix", "udg", "tls", "ssl", "tlsv1.0", "tlsv1.1", + "tlsv1.2", "tlsv1.3", + ] + }; + emit_static_string_array(ctx, transports); store_if_result(ctx, inst) } @@ -1954,6 +2007,10 @@ pub(crate) fn lower_stream_filter_remove( ctx.emitter.instruction("mov x0, #1"); // return true after removing the filter state } Arch::X86_64 => { + if ctx.emitter.target.platform == Platform::Windows { + ctx.emitter.instruction("mov rdi, rax"); // map the opaque descriptor before clearing fixed filter tables + abi::emit_call_label(ctx.emitter, "__rt_win_stream_slot"); + } abi::emit_symbol_address(ctx.emitter, "r9", "_stream_read_filters"); // read-filter table base ctx.emitter.instruction("mov BYTE PTR [r9 + rax], 0"); // clear the read-direction filter slot for this descriptor abi::emit_symbol_address(ctx.emitter, "r9", "_stream_write_filters"); // write-filter table base @@ -2409,8 +2466,11 @@ pub(crate) fn lower_stream_socket_client( abi::emit_call_label(ctx.emitter, "__rt_stream_socket_client"); ctx.emitter.instruction("ldr x1, [sp, #0]"); // reload the address pointer for host stashing ctx.emitter.instruction("ldr x2, [sp, #8]"); // reload the address byte length for host stashing - ctx.emitter.instruction("add sp, sp, #16"); // release the address scratch storage abi::emit_call_label(ctx.emitter, "__rt_stash_connect_host"); + if address_may_select_tls(ctx, address) { + emit_stream_client_crypto_on_connect_aarch64(ctx); + } + ctx.emitter.instruction("add sp, sp, #16"); // release the address scratch storage } Arch::X86_64 => { ctx.emitter.instruction("sub rsp, 16"); // reserve scratch storage for the original address string @@ -2422,14 +2482,192 @@ pub(crate) fn lower_stream_socket_client( ctx.emitter.instruction("mov rdi, rax"); // pass the connected fd to the host-stash helper ctx.emitter.instruction("mov rsi, QWORD PTR [rsp + 0]"); // reload the address pointer for host stashing ctx.emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // reload the address byte length for host stashing - ctx.emitter.instruction("add rsp, 16"); // release the address scratch storage abi::emit_call_label(ctx.emitter, "__rt_stash_connect_host"); + if address_may_select_tls(ctx, address) { + emit_stream_client_crypto_on_connect_x86_64(ctx); + } + ctx.emitter.instruction("add rsp, 16"); // release the address scratch storage } } box_stream_fd_or_false_result(ctx, "stream_socket_client"); store_if_result(ctx, inst) } +/// Returns the compile-time string a value holds, when it comes from a `ConstStr`. +/// +/// Used to decide whether a stream address can select a crypto transport before +/// emitting the handshake-on-connect path. Reading the literal here keeps the +/// decision aligned with `stream_socket_client_requirements`, which inspects the +/// same literal on the AST side: skipping the path is what lets a program with a +/// plain `tcp://` address avoid linking the TLS bridge at all. +fn const_string_operand<'m>(ctx: &FunctionContext<'m>, value: ValueId) -> Option<&'m str> { + let instruction = ctx + .function + .instructions + .iter() + .find(|instruction| instruction.result == Some(value))?; + if instruction.op != crate::ir::Op::ConstStr { + return None; + } + let crate::ir::Immediate::Data(data) = instruction.immediate.as_ref()? else { + return None; + }; + ctx.module + .data + .strings + .get(data.as_raw() as usize) + .map(String::as_str) +} + +/// Reports whether an address operand may select one of PHP's crypto transports. +/// +/// A literal address answers definitively. Anything else -- a variable, a +/// concatenation, a function result -- could carry any scheme at run time, so the +/// handshake path must be emitted. +fn address_may_select_tls(ctx: &FunctionContext<'_>, address: ValueId) -> bool { + match const_string_operand(ctx, address) { + Some(literal) => crate::builtins::address_selects_tls_transport(literal), + None => true, + } +} + +/// php's `default_socket_timeout` default, in seconds. +/// +/// php reads the ini setting; elephc has no ini layer for it yet, so the default +/// stands in. Naming it keeps the one place that would have to change. +const DEFAULT_SOCKET_TIMEOUT_SECONDS: u32 = 60; + +/// Bounds the handshake that `stream_socket_client()` runs on a crypto transport. +/// +/// Without this the handshake blocks forever against a peer that completes the TCP +/// connect and then says nothing: rustls waits on a read that never arrives, and +/// the descriptor carries no timeout. Measured against php 8.5.6 with a peer that +/// accepts and stays silent -- php returns `false` after `default_socket_timeout` +/// (10.0s with the ini set to 10), elephc never returned at all. A hung client also +/// costs far more than a failing one: it takes a whole CI shard down with it. +/// +/// php gets this for free because the stream carries `default_socket_timeout` as +/// its read timeout, and `php_stream_xport_crypto_enable` negotiates through the +/// stream. The descriptor is already staged in the attach frame at `[sp]`, so the +/// timeout is applied from there and register clobbering is irrelevant. A plain +/// file descriptor makes `setsockopt` fail, which the helper reports as 0 and this +/// path ignores -- the handshake is what needs bounding, not the syscall. +fn emit_crypto_handshake_deadline(ctx: &mut FunctionContext<'_>) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("ldr x0, [sp, #0]"); // the descriptor staged for the attach + ctx.emitter + .instruction(&format!("mov x1, #{}", DEFAULT_SOCKET_TIMEOUT_SECONDS)); // php's default_socket_timeout + ctx.emitter.instruction("mov x2, #0"); // no sub-second component + abi::emit_call_label(ctx.emitter, "__rt_stream_set_timeout"); + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 0]"); // the descriptor staged for the attach + ctx.emitter + .instruction(&format!("mov rsi, {}", DEFAULT_SOCKET_TIMEOUT_SECONDS)); // php's default_socket_timeout + ctx.emitter.instruction("mov rdx, 0"); // no sub-second component + abi::emit_call_label(ctx.emitter, "__rt_stream_set_timeout"); + } + } +} + +/// Emits the AArch64 handshake-on-connect path for `stream_socket_client()`. +/// +/// php-src registers `ssl`, `sslv3`, `tls` and `tlsv1.0`..`tlsv1.3` as crypto +/// transports whose factory sets `sslsock->enable_on_connect = 1` +/// (`php_openssl_ssl_socket_factory`, ext/openssl/xp_ssl.c), so the TLS handshake +/// runs *inside* `stream_socket_client()` and a failed handshake makes the call +/// return `false`. Without this the function handed back a working plaintext +/// socket for `tls://`, silently downgrading every such connection. +/// +/// Entry: the integer result register holds the connected descriptor, and the +/// caller's 16-byte scratch frame still holds the address pointer at `[sp, #0]` +/// and its length at `[sp, #8]`. That frame is reused to carry the descriptor +/// across the attach, so it must stay live until this returns. Exit: the result +/// register holds the descriptor to expose, or -1 when the handshake failed. +/// +/// Unlike `stream_socket_enable_crypto()`, which reports 0 for "call me again" +/// on a non-blocking stream, this path treats any non-positive status as failure: +/// `stream_socket_client()` connects blocking, so a handshake that has not +/// completed by the time the bridge returns never will -- a peer that closes +/// mid-handshake surfaces as 0, and handing back a stream there would be the very +/// plaintext downgrade this path exists to prevent. +fn emit_stream_client_crypto_on_connect_aarch64(ctx: &mut FunctionContext<'_>) { + let plain = ctx.next_label("ssc_plain"); + let restore = ctx.next_label("ssc_restore_fd"); + let attached = ctx.next_label("ssc_attached"); + let failed = ctx.next_label("ssc_tls_failed"); + ctx.emitter.instruction("cmp x0, #0"); // did the connect itself fail? + ctx.emitter.instruction(&format!("b.lt {}", plain)); // nothing to negotiate on a failed connect + ctx.emitter.instruction("mov x3, x0"); // hold the descriptor while the address is probed + ctx.emitter.instruction("ldr x0, [sp, #0]"); // address pointer + ctx.emitter.instruction("ldr x1, [sp, #8]"); // address byte length + ctx.emitter.instruction("str x3, [sp, #0]"); // the descriptor replaces the pointer in the scratch slot + abi::emit_call_label(ctx.emitter, "__rt_addr_tls_crypto_method"); + ctx.emitter.instruction(&format!("cbz x0, {}", restore)); // zero means a plain transport + ctx.emitter.instruction("mov x4, x0"); // preserve the scheme's PHP crypto method + abi::emit_reserve_temporary_stack(ctx.emitter, 32); + ctx.emitter.instruction("ldr x9, [sp, #32]"); // reload the descriptor from the caller scratch frame + ctx.emitter.instruction("str x9, [sp, #0]"); // descriptor slot expected by the shared attach path + ctx.emitter.instruction("str x4, [sp, #8]"); // the transport pins the crypto method + ctx.emitter.instruction("mov x9, #1"); // the method is always explicit here + ctx.emitter.instruction("str x9, [sp, #16]"); // the bridge rejects an absent method outright + ctx.emitter.instruction("str xzr, [sp, #24]"); // no session is being resumed + emit_crypto_handshake_deadline(ctx); + lower_stream_socket_enable_crypto_attach_aarch64(ctx, &attached); + ctx.emitter.label(&attached); + ctx.emitter.instruction("cmp x0, #0"); // 1 completed, 0 still handshaking, negative terminal + ctx.emitter.instruction(&format!("b.le {}", failed)); // only a completed handshake may hand back a stream + ctx.emitter.label(&restore); + ctx.emitter.instruction("ldr x0, [sp, #0]"); // hand back the descriptor, not the probe result + ctx.emitter.instruction(&format!("b {}", plain)); // join the common exit + ctx.emitter.label(&failed); + ctx.emitter.instruction("ldr x0, [sp, #0]"); // close the socket the failed session no longer owns + ctx.emitter.bl_c("close"); // php does not leak the descriptor on failure + ctx.emitter.instruction("mov x0, #-1"); // the false sentinel understood by the result boxing + ctx.emitter.label(&plain); +} + +/// Emits the x86_64 handshake-on-connect path for `stream_socket_client()`. +/// +/// Mirrors `emit_stream_client_crypto_on_connect_aarch64`; see it for the php-src +/// reference and the scratch-frame contract. +fn emit_stream_client_crypto_on_connect_x86_64(ctx: &mut FunctionContext<'_>) { + let plain = ctx.next_label("ssc_plain_x"); + let restore = ctx.next_label("ssc_restore_fd_x"); + let attached = ctx.next_label("ssc_attached_x"); + let failed = ctx.next_label("ssc_tls_failed_x"); + ctx.emitter.instruction("cmp rax, 0"); // did the connect itself fail? + ctx.emitter.instruction(&format!("jl {}", plain)); // nothing to negotiate on a failed connect + ctx.emitter.instruction("mov rcx, rax"); // hold the descriptor while the address is probed + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 0]"); // address pointer + ctx.emitter.instruction("mov rsi, QWORD PTR [rsp + 8]"); // address byte length + ctx.emitter.instruction("mov QWORD PTR [rsp + 0], rcx"); // the descriptor replaces the pointer in the scratch slot + abi::emit_call_label(ctx.emitter, "__rt_addr_tls_crypto_method"); + ctx.emitter.instruction("test rax, rax"); // zero means a plain transport + ctx.emitter.instruction(&format!("jz {}", restore)); // leave the socket in the clear + ctx.emitter.instruction("mov r8, rax"); // preserve the scheme's PHP crypto method + abi::emit_reserve_temporary_stack(ctx.emitter, 32); + ctx.emitter.instruction("mov r9, QWORD PTR [rsp + 32]"); // reload the descriptor from the caller scratch frame + ctx.emitter.instruction("mov QWORD PTR [rsp + 0], r9"); // descriptor slot expected by the shared attach path + ctx.emitter.instruction("mov QWORD PTR [rsp + 8], r8"); // the transport pins the crypto method + ctx.emitter.instruction("mov QWORD PTR [rsp + 16], 1"); // the bridge rejects an absent method outright + ctx.emitter.instruction("mov QWORD PTR [rsp + 24], 0"); // no session is being resumed + emit_crypto_handshake_deadline(ctx); + lower_stream_socket_enable_crypto_attach_x86_64(ctx, &attached); + ctx.emitter.label(&attached); + ctx.emitter.instruction("cmp rax, 0"); // 1 completed, 0 still handshaking, negative terminal + ctx.emitter.instruction(&format!("jle {}", failed)); // only a completed handshake may hand back a stream + ctx.emitter.label(&restore); + ctx.emitter.instruction("mov rax, QWORD PTR [rsp + 0]"); // hand back the descriptor, not the probe result + ctx.emitter.instruction(&format!("jmp {}", plain)); // join the common exit + ctx.emitter.label(&failed); + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 0]"); // close the socket the failed session no longer owns + ctx.emitter.emit_call_c("close"); // php does not leak the descriptor on failure + ctx.emitter.instruction("mov rax, -1"); // the false sentinel understood by the result boxing + ctx.emitter.label(&plain); +} + /// Lowers `stream_socket_accept(server, timeout?, peer_name?)`. pub(crate) fn lower_stream_socket_accept( ctx: &mut FunctionContext<'_>, @@ -2550,15 +2788,72 @@ pub(crate) fn lower_stream_socket_enable_crypto( let stream = expect_operand(inst, 0)?; let enable = expect_operand(inst, 1)?; load_stream_fd_to_result(ctx, stream, "stream_socket_enable_crypto")?; - abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + abi::emit_reserve_temporary_stack(ctx.emitter, 32); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("str x0, [sp, #0]"); // preserve the descriptor beside crypto-method metadata + } + Arch::X86_64 => { + ctx.emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // preserve the descriptor beside crypto-method metadata + } + } require_int_or_bool( ctx.load_value_to_result(enable)?.codegen_repr(), "stream_socket_enable_crypto enable", )?; abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); - for index in 2..inst.operands.len() { - let operand = expect_operand(inst, index)?; - ctx.load_value_to_result(operand)?; + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("str xzr, [sp, #24]"); // initialize the optional crypto-method value + ctx.emitter.instruction("str xzr, [sp, #32]"); // mark the method argument absent or null + ctx.emitter.instruction("str xzr, [sp, #40]"); // initialize the optional source TLS session handle + } + Arch::X86_64 => { + ctx.emitter.instruction("mov QWORD PTR [rsp + 24], 0"); // initialize the optional crypto-method value + ctx.emitter.instruction("mov QWORD PTR [rsp + 32], 0"); // mark the method argument absent or null + ctx.emitter.instruction("mov QWORD PTR [rsp + 40], 0"); // initialize the optional source TLS session handle + } + } + if inst.operands.len() >= 3 { + let method = expect_operand(inst, 2)?; + if !is_nullish_value(ctx, method)? { + require_int( + ctx.load_value_to_result(method)?.codegen_repr(), + "stream_socket_enable_crypto crypto_method", + )?; + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("str x0, [sp, #24]"); // preserve the requested crypto method beside the descriptor + ctx.emitter.instruction("mov x9, #1"); // mark the explicit method argument present + ctx.emitter.instruction("str x9, [sp, #32]"); // preserve explicit zero separately from absence + } + Arch::X86_64 => { + ctx.emitter.instruction("mov QWORD PTR [rsp + 24], rax"); // preserve the requested crypto method beside the descriptor + ctx.emitter.instruction("mov QWORD PTR [rsp + 32], 1"); // preserve explicit zero separately from absence + } + } + } + } + if inst.operands.len() >= 4 { + let session_stream = expect_operand(inst, 3)?; + if !is_nullish_value(ctx, session_stream)? { + load_stream_fd_to_result( + ctx, + session_stream, + "stream_socket_enable_crypto session_stream", + )?; + match ctx.emitter.target.arch { + Arch::AArch64 => { + abi::emit_call_label(ctx.emitter, "__rt_tls_session_get"); + ctx.emitter.instruction("str x0, [sp, #40]"); // preserve the reusable source TLS session handle + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rdi, rax"); // pass the source descriptor to the TLS session table + abi::emit_call_label(ctx.emitter, "__rt_tls_session_get"); + ctx.emitter.instruction("mov QWORD PTR [rsp + 40], rax"); // preserve the reusable source TLS session handle + } + } + } } match ctx.emitter.target.arch { Arch::AArch64 => abi::emit_pop_reg(ctx.emitter, "x0"), @@ -2571,7 +2866,7 @@ pub(crate) fn lower_stream_socket_enable_crypto( ctx.emitter.instruction(&format!("cbnz x0, {}", enable_label)); // enable=true enters the TLS attach path ctx.emitter.instruction("ldr x0, [sp]"); // reload the stashed descriptor for TLS teardown emit_tls_session_teardown_for_current_fd(ctx); - abi::emit_release_temporary_stack(ctx.emitter, 16); + abi::emit_release_temporary_stack(ctx.emitter, 32); ctx.emitter.instruction("mov x0, #1"); // disabling crypto succeeds even when no session exists ctx.emitter.instruction(&format!("b {}", done_label)); // skip the TLS attach path ctx.emitter.label(&enable_label); @@ -2582,7 +2877,7 @@ pub(crate) fn lower_stream_socket_enable_crypto( ctx.emitter.instruction(&format!("jnz {}", enable_label)); // enable=true enters the TLS attach path ctx.emitter.instruction("mov rax, QWORD PTR [rsp]"); // reload the stashed descriptor for TLS teardown emit_tls_session_teardown_for_current_fd(ctx); - abi::emit_release_temporary_stack(ctx.emitter, 16); + abi::emit_release_temporary_stack(ctx.emitter, 32); ctx.emitter.instruction("mov eax, 1"); // disabling crypto succeeds even when no session exists ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the TLS attach path ctx.emitter.label(&enable_label); @@ -2590,6 +2885,7 @@ pub(crate) fn lower_stream_socket_enable_crypto( } } ctx.emitter.label(&done_label); + box_stream_socket_enable_crypto_status(ctx); store_if_result(ctx, inst) } @@ -2746,10 +3042,21 @@ pub(crate) fn lower_fclose(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> emit_bz2_flush_on_close_for_current_fd(ctx); emit_iconv_flush_on_close_for_current_fd(ctx); emit_tls_session_teardown_for_current_fd(ctx); - if matches!(ctx.emitter.target.arch, Arch::X86_64) { - ctx.emitter.instruction("mov rdi, rax"); // pass the descriptor to the user-filter teardown helper + if ctx.emitter.target.platform != Platform::Windows { + if matches!(ctx.emitter.target.arch, Arch::X86_64) { + // The descriptor lives in rax here, and this helper reads it from rdi. + // Staging it looks redundant because rdi usually still holds the fd from + // the TLS teardown's own map lookup -- but only while that teardown finds + // no session to close. When it does find one it loads the session handle + // into rdi and calls the bridge, and the close leaves rdi holding whatever + // it pleases. `__rt_user_filter_release_fd` then indexes its instance + // table with that value instead of a descriptor, which is how closing a + // TLS stream came to fault on linux-x86_64 while closing a plain one -- + // and every other fd-taking call in this function stages rdi explicitly. + ctx.emitter.instruction("mov rdi, rax"); // pass the stream fd to the user-filter release helper + } + abi::emit_call_label(ctx.emitter, "__rt_user_filter_release_fd"); } - abi::emit_call_label(ctx.emitter, "__rt_user_filter_release_fd"); match ctx.emitter.target.arch { Arch::AArch64 => { abi::emit_symbol_address(ctx.emitter, "x9", "_stream_read_filters"); @@ -2765,10 +3072,12 @@ pub(crate) fn lower_fclose(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> ctx.emitter.instruction("mov x0, #1"); // return true when the stream close succeeded } Arch::X86_64 => { - abi::emit_symbol_address(ctx.emitter, "r9", "_stream_read_filters"); // read-filter table base - ctx.emitter.instruction("mov BYTE PTR [r9 + rax], 0"); // clear any read filter before the descriptor can be reused - abi::emit_symbol_address(ctx.emitter, "r9", "_stream_write_filters"); // write-filter table base - ctx.emitter.instruction("mov BYTE PTR [r9 + rax], 0"); // clear any write filter before the descriptor can be reused + if ctx.emitter.target.platform != Platform::Windows { + abi::emit_symbol_address(ctx.emitter, "r9", "_stream_read_filters"); // read-filter table base + ctx.emitter.instruction("mov BYTE PTR [r9 + rax], 0"); // clear any read filter before the descriptor can be reused + abi::emit_symbol_address(ctx.emitter, "r9", "_stream_write_filters"); // write-filter table base + ctx.emitter.instruction("mov BYTE PTR [r9 + rax], 0"); // clear any write filter before the descriptor can be reused + } ctx.emitter.instruction("mov rdi, rax"); // pass the stream fd to libc close() ctx.emitter.instruction("call close"); // close the requested stream descriptor ctx.emitter.instruction("cmp rax, 0"); // test whether close() reported success @@ -3636,6 +3945,455 @@ pub(crate) fn lower_pclose(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> store_if_result(ctx, inst) } +/// Lowers PHP's `proc_open(command, descriptor_spec, pipes, cwd?, env_vars?, +/// options?)` and boxes the process as `resource|false`. The checker currently +/// permits only a string command and null/omitted optional settings; this lowerer +/// preserves the existing pipe runtime ABI while accepting the complete arity. +/// +/// Runtime ABI: descriptor, command pointer/length, pipes, cwd pointer/length, +/// environment-block pointer, and packed environment-length/direct flags. +/// AArch64 uses `x0..x7`. x86_64 uses `rdi/rsi/rdx/rcx/r8/r9` plus the final +/// two values in SysV stack-argument slots. Omitted values are transported as zero. +pub(crate) fn lower_proc_open(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + if inst.operands.len() != 9 { + ensure_arg_count_between(inst, "proc_open", 3, 6)?; + } + let public_command = expect_operand(inst, 0)?; + let command_override = inst.operands.get(6).copied(); + let command = match command_override { + Some(value) if ctx.value_php_type(value)? == PhpType::Str => value, + _ => public_command, + }; + let descriptor_spec = expect_operand(inst, 1)?; + let pipes = expect_operand(inst, 2)?; + // `__rt_proc_open` returns the possibly promoted/reallocated `$pipes` + // container beside the process result. A direct ref parameter normally + // originates in a local load; keep that slot so the new pointer survives + // a sparse descriptor key (which promotes `[]` to hash storage). + let pipes_local = source_load_local_slot(ctx, pipes)?; + let cwd = inst.operands.get(3).copied(); + let public_env_vars = inst.operands.get(4).copied(); + let public_options = inst.operands.get(5).copied(); + let env_vars = if inst.operands.len() == 9 { + inst.operands.get(7).copied() + } else { + public_env_vars + }; + let packed_flags = if inst.operands.len() == 9 { + inst.operands.get(8).copied() + } else { + public_options + }; + match ctx.emitter.target.arch { + Arch::AArch64 => { + // -- load and stack the descriptor_spec array pointer (x0) -- + ctx.load_value_to_result(descriptor_spec)?; + abi::emit_push_reg(ctx.emitter, "x0"); + // -- load and stack the command string (x1 ptr, x2 len) -- + load_string_to_result(ctx, command, "proc_open command")?; + abi::emit_push_reg_pair(ctx.emitter, "x1", "x2"); + // -- stage pipes and every optional process setting -- + ctx.load_value_to_result(pipes)?; + abi::emit_push_reg(ctx.emitter, "x0"); + load_optional_proc_open_string(ctx, cwd, "proc_open cwd")?; + abi::emit_push_reg_pair(ctx.emitter, "x1", "x2"); + load_optional_proc_open_pointer(ctx, env_vars)?; + abi::emit_push_reg(ctx.emitter, "x0"); + load_optional_proc_open_pointer(ctx, packed_flags)?; + abi::emit_push_reg(ctx.emitter, "x0"); + // -- restore the complete x0..x7 runtime ABI in reverse order -- + abi::emit_pop_reg(ctx.emitter, "x7"); + abi::emit_pop_reg(ctx.emitter, "x6"); + abi::emit_pop_reg_pair(ctx.emitter, "x4", "x5"); + abi::emit_pop_reg(ctx.emitter, "x3"); + abi::emit_pop_reg_pair(ctx.emitter, "x1", "x2"); + abi::emit_pop_reg(ctx.emitter, "x0"); + } + Arch::X86_64 => { + ctx.emitter.instruction("sub rsp, 48"); // reserve dynamic ownership, result, and validation slots + ctx.emitter.instruction("mov QWORD PTR [rsp], 0"); // no owned command buffer unless runtime argv marshalling runs + ctx.emitter.instruction("mov QWORD PTR [rsp + 8], 0"); // no owned environment block unless runtime hash marshalling runs + ctx.emitter.instruction("mov QWORD PTR [rsp + 32], 0"); // dynamic command marshalling has not failed + // -- load and stack the descriptor_spec array pointer (rax) -- + ctx.load_value_to_result(descriptor_spec)?; + abi::emit_push_reg(ctx.emitter, "rax"); + // -- load and stack the command string (rax ptr, rdx len) -- + if matches!( + ctx.value_php_type(command)?, + PhpType::Array(_) | PhpType::AssocArray { .. } + ) { + ctx.load_value_to_result(command)?; + ctx.emitter.instruction("mov rdi, rax"); // pass the runtime argv array to the Windows marshaller + abi::emit_call_label(ctx.emitter, "__rt_win_proc_command_array"); + ctx.emitter.instruction("mov QWORD PTR [rsp + 16], rax"); // retain the allocation above the 16-byte staged descriptor slot + ctx.emitter.instruction("test rax, rax"); // did argv marshalling succeed? + let command_valid = ctx.next_label("proc_open_command_valid"); + ctx.emitter.instruction(&format!("jnz {command_valid}")); // non-null buffer is a valid command line + ctx.emitter.instruction("mov QWORD PTR [rsp + 48], 1"); // record failure in the fixed validation slot + ctx.emitter.label(&command_valid); + } else { + load_string_to_result(ctx, command, "proc_open command")?; + } + abi::emit_push_reg_pair(ctx.emitter, "rax", "rdx"); + // -- stage pipes and every optional process setting -- + ctx.load_value_to_result(pipes)?; + abi::emit_push_reg(ctx.emitter, "rax"); + load_optional_proc_open_string(ctx, cwd, "proc_open cwd")?; + abi::emit_push_reg_pair(ctx.emitter, "rax", "rdx"); + let dynamic_environment = public_env_vars + .filter(|_| inst.operands.len() == 9) + .filter(|value| { + matches!( + ctx.value_php_type(*value), + Ok(PhpType::Array(_) | PhpType::AssocArray { .. }) + ) + }); + if let Some(environment) = dynamic_environment { + ctx.load_value_to_result(environment)?; + ctx.emitter.instruction("mov rdi, rax"); // pass computed environment storage to the runtime marshaller + abi::emit_call_label(ctx.emitter, "__rt_win_proc_environment"); + ctx.emitter.instruction("mov QWORD PTR [rsp + 72], rax"); // retain the owned environment block above four staged ABI slots + ctx.emitter.instruction("mov QWORD PTR [rsp + 88], rdx"); // preserve its counted byte length for packed flags + } else { + load_optional_proc_open_pointer(ctx, env_vars)?; + } + abi::emit_push_reg(ctx.emitter, "rax"); + load_optional_proc_open_pointer(ctx, packed_flags)?; + if matches!( + ctx.value_php_type(command)?, + PhpType::Array(_) | PhpType::AssocArray { .. } + ) { + ctx.emitter.instruction("or rax, 1"); // runtime command arrays always bypass cmd.exe wrapping + } + if dynamic_environment.is_some() { + ctx.emitter.instruction("mov rdx, QWORD PTR [rsp + 104]"); // reload dynamic environment byte length after stacking its pointer + ctx.emitter.instruction("cmp rdx, -1"); // marshalling failure sentinel? + let environment_valid = ctx.next_label("proc_open_environment_valid"); + let environment_flags_ready = ctx.next_label("proc_open_environment_flags_ready"); + ctx.emitter.instruction(&format!("jne {environment_valid}")); // valid length can be packed normally + ctx.emitter.instruction("bts rax, 63"); // mark runtime ABI invalid while preserving helper errno + ctx.emitter.instruction(&format!("jmp {environment_flags_ready}")); // skip length packing after failure + ctx.emitter.label(&environment_valid); + ctx.emitter.instruction("and rax, 31"); // retain all five Windows proc_open option bits + ctx.emitter.instruction("shl rdx, 5"); // pack environment length above the option bits + ctx.emitter.instruction("or rax, rdx"); // combine environment length and Windows options + ctx.emitter.label(&environment_flags_ready); + } + ctx.emitter.instruction("cmp QWORD PTR [rsp + 112], 0"); // did dynamic command marshalling fail before flags were loaded? + let command_flags_ready = ctx.next_label("proc_open_command_flags_ready"); + ctx.emitter.instruction(&format!("je {command_flags_ready}")); // valid/static commands leave flags unchanged + ctx.emitter.instruction("bts rax, 63"); // propagate argv validation failure without overwriting errno + ctx.emitter.label(&command_flags_ready); + if let Some(options) = public_options.filter(|_| inst.operands.len() == 9) { + if matches!( + ctx.value_php_type(options)?, + PhpType::Array(_) | PhpType::AssocArray { .. } + ) { + abi::emit_push_reg(ctx.emitter, "rax"); + ctx.load_value_to_result(options)?; + ctx.emitter.instruction("mov rdi, rax"); // pass computed proc_open options to the runtime validator + abi::emit_call_label(ctx.emitter, "__rt_win_proc_options"); + ctx.emitter.instruction("mov r10, rax"); // retain the dynamic Windows option-bit mask + abi::emit_pop_reg(ctx.emitter, "rax"); + ctx.emitter.instruction("cmp r10, -1"); // did runtime option validation fail? + let options_valid = ctx.next_label("proc_open_options_valid"); + ctx.emitter.instruction(&format!("jne {options_valid}")); // valid false/true values continue normally + ctx.emitter.instruction("bts rax, 63"); // mark the packed ABI invalid without losing errno + ctx.emitter.label(&options_valid); + ctx.emitter.instruction("or rax, r10"); // merge recognized dynamic Windows options + } + } + abi::emit_push_reg(ctx.emitter, "rax"); + // -- restore register args and retain env/options as SysV stack args -- + abi::emit_pop_reg(ctx.emitter, "r11"); + abi::emit_pop_reg(ctx.emitter, "r10"); + abi::emit_pop_reg_pair(ctx.emitter, "r8", "r9"); + abi::emit_pop_reg(ctx.emitter, "rcx"); + abi::emit_pop_reg_pair(ctx.emitter, "rsi", "rdx"); + abi::emit_pop_reg(ctx.emitter, "rdi"); + ctx.emitter.instruction("sub rsp, 16"); // reserve aligned SysV stack-argument slots 7 and 8 + ctx.emitter.instruction("mov QWORD PTR [rsp], r10"); // arg7 = env_vars array pointer (or null) + ctx.emitter.instruction("mov QWORD PTR [rsp + 8], r11"); // arg8 = options array pointer (or null) + } + } + abi::emit_call_label(ctx.emitter, "__rt_proc_open"); + // Register the returned `$pipes` container before process result boxing. + // The runtime retains the container by refcount and, at proc_close time, + // marks/closes exactly its kind-1 pipe resources before waiting. This is + // required to avoid deadlock when a child fills an unread output pipe. + match ctx.emitter.target.arch { + Arch::AArch64 => { + abi::emit_call_label(ctx.emitter, "__rt_proc_pipe_registry_register"); + abi::emit_push_reg_pair(ctx.emitter, "x0", "x1"); + load_string_to_result(ctx, command, "proc_open status command")?; + ctx.emitter.instruction("ldr x0, [sp]"); // restore the raw process PID for status registration + abi::emit_call_label(ctx.emitter, "__rt_proc_status_register"); + abi::emit_pop_reg_pair(ctx.emitter, "x0", "x1"); + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rdi, rax"); // pass the raw pid/process HANDLE to the pipe registry + ctx.emitter.instruction("mov rsi, rdx"); // pass the paired final pipes container to the registry + abi::emit_call_label(ctx.emitter, "__rt_proc_pipe_registry_register"); + } + } + if let Some(slot) = pipes_local { + // Every target returns the final container beside the process result. + // This matters for sparse descriptor keys: the generic keyed setter + // promotes an initially indexed `$pipes = []` to hash storage. + store_proc_open_pipes_result(ctx, slot)?; + } + if ctx.emitter.target.arch == Arch::X86_64 { + ctx.emitter.instruction("add rsp, 16"); // release the two optional stack-argument slots + if matches!( + ctx.value_php_type(command)?, + PhpType::Array(_) | PhpType::AssocArray { .. } + ) { + if ctx.emitter.target.platform == Platform::Windows { + ctx.emitter.instruction("mov rdi, rax"); // pass the raw process HANDLE to the Windows status registry + ctx.emitter.instruction("mov rsi, QWORD PTR [rsp]"); // pass the still-owned marshalled argv command line + abi::emit_call_label(ctx.emitter, "__rt_proc_status_register_cstr"); + } else { + return Err(CodegenIrError::unsupported( + "proc_open() array commands require the Windows status registry", + )); + } + } else { + ctx.emitter.instruction("mov QWORD PTR [rsp + 16], rax"); // preserve the process descriptor while loading its source command + load_string_to_result(ctx, command, "proc_open status command")?; + ctx.emitter.instruction("mov rsi, rax"); // pass the command pointer to the status registry + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 16]"); // restore the raw process descriptor after string materialization + abi::emit_call_label(ctx.emitter, "__rt_proc_status_register"); + } + ctx.emitter.instruction("mov QWORD PTR [rsp + 16], rax"); // preserve the process result across marshalling-buffer cleanup + ctx.emitter.instruction("mov rax, QWORD PTR [rsp]"); // load the optional owned dynamic command buffer + ctx.emitter.instruction("test rax, rax"); // did runtime argv marshalling allocate a command line? + let skip_dynamic_command_free = ctx.next_label("proc_open_command_free_done"); + ctx.emitter.instruction(&format!("jz {skip_dynamic_command_free}")); // static string commands own no staging buffer here + abi::emit_call_label(ctx.emitter, "__rt_heap_free"); + ctx.emitter.label(&skip_dynamic_command_free); + ctx.emitter.instruction("mov rax, QWORD PTR [rsp + 8]"); // load the optional owned environment block + ctx.emitter.instruction("test rax, rax"); // was a custom dynamic environment marshalled? + let skip_dynamic_environment_free = ctx.next_label("proc_open_environment_free_done"); + ctx.emitter.instruction(&format!("jz {skip_dynamic_environment_free}")); // inherited/static environments own no block here + abi::emit_call_label(ctx.emitter, "__rt_heap_free"); + ctx.emitter.label(&skip_dynamic_environment_free); + ctx.emitter.instruction("mov rax, QWORD PTR [rsp + 16]"); // restore the process result + ctx.emitter.instruction("add rsp, 48"); // release dynamic marshalling ownership state + } + box_stream_fd_or_false_result_kind(ctx, "proc_open", 5); + store_if_result(ctx, inst) +} + +/// Stores `proc_open`'s secondary container result through its by-reference +/// `$pipes` local while preserving the primary process result. +/// +/// The runtime returns the process descriptor in the normal integer result +/// register and the final pipes container in the paired secondary register: +/// `x0`/`x1` on AArch64 and `rax`/`rdx` on x86_64. Using +/// `store_current_result_to_local` makes raw locals, definite ref-cells, and +/// dynamically promoted ref-cells share the established ownership-safe store +/// path. +fn store_proc_open_pipes_result( + ctx: &mut FunctionContext<'_>, + slot: LocalSlotId, +) -> Result<()> { + match ctx.emitter.target.arch { + Arch::AArch64 => { + abi::emit_push_reg(ctx.emitter, "x0"); + ctx.emitter.instruction("mov x0, x1"); // move the returned pipes container into the canonical local-store register + ctx.store_current_result_to_local(slot)?; + abi::emit_pop_reg(ctx.emitter, "x0"); + } + Arch::X86_64 => { + abi::emit_push_reg(ctx.emitter, "rax"); + ctx.emitter.instruction("mov rax, rdx"); // move the returned pipes container into the canonical local-store register + ctx.store_current_result_to_local(slot)?; + abi::emit_pop_reg(ctx.emitter, "rax"); + } + } + Ok(()) +} + +/// Loads an optional proc_open string into the target string-result registers, +/// using a null pointer/zero length for omitted or explicit-null arguments. +fn load_optional_proc_open_string( + ctx: &mut FunctionContext<'_>, + value: Option, + what: &str, +) -> Result<()> { + let Some(value) = value else { + zero_proc_open_string_result(ctx); + return Ok(()); + }; + if matches!(ctx.value_php_type(value)?, PhpType::Void | PhpType::Never) { + zero_proc_open_string_result(ctx); + return Ok(()); + } + load_string_to_result(ctx, value, what) +} + +/// Writes a null string pair into the target string-result registers. +fn zero_proc_open_string_result(ctx: &mut FunctionContext<'_>) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x1, #0"); // optional string pointer = null + ctx.emitter.instruction("mov x2, #0"); // optional string length = zero + } + Arch::X86_64 => { + ctx.emitter.instruction("xor eax, eax"); // optional string pointer = null + ctx.emitter.instruction("xor edx, edx"); // optional string length = zero + } + } +} + +/// Loads an optional array/settings pointer into the integer result register, +/// using null for omitted or explicit-null arguments. +fn load_optional_proc_open_pointer( + ctx: &mut FunctionContext<'_>, + value: Option, +) -> Result<()> { + let Some(value) = value else { + zero_proc_open_pointer_result(ctx); + return Ok(()); + }; + if matches!(ctx.value_php_type(value)?, PhpType::Void | PhpType::Never) { + zero_proc_open_pointer_result(ctx); + return Ok(()); + } + ctx.load_value_to_result(value).map(|_| ()) +} + +/// Writes a null optional-settings pointer into the integer result register. +fn zero_proc_open_pointer_result(ctx: &mut FunctionContext<'_>) { + match ctx.emitter.target.arch { + Arch::AArch64 => ctx.emitter.instruction("mov x0, #0"), // optional settings pointer = null + Arch::X86_64 => ctx.emitter.instruction("xor eax, eax"), // optional settings pointer = null + } +} + +/// Lowers `proc_close(process)` and returns the child process exit status. +pub(crate) fn lower_proc_close(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + super::ensure_arg_count(inst, "proc_close", 1)?; + let handle = expect_operand(inst, 0)?; + let captured = capture_resource_box_for_release(ctx, handle)?; + load_stream_fd_to_result(ctx, handle, "proc_close")?; + apply_resource_release_sentinel(ctx, captured); + if ctx.emitter.target.arch == Arch::X86_64 { + ctx.emitter.instruction("mov rdi, rax"); // pass the process descriptor to the runtime close helper + } + abi::emit_call_label(ctx.emitter, "__rt_proc_close"); + store_if_result(ctx, inst) +} + +/// Lowers `proc_get_status(process)` through retained target process metadata. +/// +/// The platform runtime preserves the process handle/PID and follows PHP's +/// non-consuming status contract: Windows asks `GetExitCodeProcess`, while Unix +/// caches a completed `wait4(WNOHANG|WUNTRACED)` observation for later queries. +pub(crate) fn lower_proc_get_status( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + super::ensure_arg_count(inst, "proc_get_status", 1)?; + let process = expect_operand(inst, 0)?; + load_stream_fd_to_result(ctx, process, "proc_get_status")?; + if ctx.emitter.target.arch == Arch::X86_64 { + ctx.emitter.instruction("mov rdi, rax"); // pass the retained process descriptor to the status runtime + } + abi::emit_call_label(ctx.emitter, "__rt_proc_get_status"); + box_stat_array_or_false_result(ctx); + store_if_result(ctx, inst) +} + +/// Lowers `proc_terminate(process, signal = SIGTERM)` without consuming the process resource. +/// +/// Unix forwards the requested signal to `kill(2)`. Windows transports the +/// argument for a common ABI but the runtime deliberately ignores it and uses +/// php-src's `TerminateProcess(handle, 255)` behavior. +pub(crate) fn lower_proc_terminate( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + super::ensure_arg_count_between(inst, "proc_terminate", 1, 2)?; + let process = expect_operand(inst, 0)?; + load_stream_fd_to_result(ctx, process, "proc_terminate")?; + match ctx.emitter.target.arch { + Arch::AArch64 => { + abi::emit_push_reg(ctx.emitter, "x0"); + if let Some(signal) = inst.operands.get(1).copied() { + load_proc_terminate_signal_as_int(ctx, signal, 16)?; + } else { + ctx.emitter.instruction("mov x0, #15"); // default PHP signal is SIGTERM + } + ctx.emitter.instruction("mov x1, x0"); // second runtime argument is the requested Unix signal + abi::emit_pop_reg(ctx.emitter, "x0"); + } + Arch::X86_64 => { + ctx.emitter.instruction("sub rsp, 16"); // retain the HANDLE/PID while materializing the optional signal + ctx.emitter.instruction("mov QWORD PTR [rsp], rax"); // save the process descriptor across value loading + if let Some(signal) = inst.operands.get(1).copied() { + load_proc_terminate_signal_as_int(ctx, signal, 16)?; + } else { + ctx.emitter.instruction("mov rax, 15"); // default PHP signal is SIGTERM + } + ctx.emitter.instruction("mov rsi, rax"); // second runtime argument is the requested Unix signal + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp]"); // first runtime argument is the process descriptor + ctx.emitter.instruction("add rsp, 16"); // release the aligned staging slot before the call + } + } + abi::emit_call_label(ctx.emitter, "__rt_proc_terminate"); + store_if_result(ctx, inst) +} + +/// Loads a weakly typed PHP scalar as the integer signal expected by `proc_terminate`. +fn load_proc_terminate_signal_as_int( + ctx: &mut FunctionContext<'_>, + signal: ValueId, + process_staging_bytes: usize, +) -> Result<()> { + match ctx.load_value_to_result(signal)?.codegen_repr() { + PhpType::Int | PhpType::Bool => Ok(()), + PhpType::Void | PhpType::Never => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + Ok(()) + } + PhpType::Float => { + abi::emit_float_result_to_int_result(ctx.emitter); + Ok(()) + } + PhpType::TaggedScalar => { + crate::codegen::sentinels::emit_tagged_scalar_to_int_null_as_zero(ctx.emitter); + Ok(()) + } + PhpType::Str => { + let invalid = ctx.next_label("proc_terminate_signal_type_error"); + let done = ctx.next_label("proc_terminate_signal_coerced"); + crate::codegen::lower_inst::enums::emit_string_result_to_int_checked( + ctx, + &invalid, + ); + abi::emit_jump(ctx.emitter, &done); + ctx.emitter.label(&invalid); + abi::emit_release_temporary_stack(ctx.emitter, 16); + abi::emit_release_temporary_stack(ctx.emitter, process_staging_bytes); + let (message_label, message_len) = ctx.data.add_string( + b"proc_terminate(): Argument #2 ($signal) must be of type int, string given", + ); + crate::codegen::lower_inst::enums::emit_throw_static_type_error( + ctx, + &message_label, + message_len, + ); + ctx.emitter.label(&done); + Ok(()) + } + other => Err(CodegenIrError::unsupported(format!( + "proc_terminate signal for PHP type {:?}", + other + ))), + } +} + /// Lowers `fsockopen(host, port, errno?, errstr?, timeout?)`. pub(crate) fn lower_fsockopen(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { ensure_arg_count_between(inst, "fsockopen", 2, 5)?; @@ -3656,6 +4414,9 @@ pub(crate) fn lower_fsockopen(ctx: &mut FunctionContext<'_>, inst: &Instruction) ctx.emitter.instruction("mov x0, x1"); // pass hostname pointer as the first runtime argument ctx.emitter.instruction("mov x1, x2"); // pass hostname byte length as the second runtime argument ctx.emitter.instruction("mov x2, x9"); // pass TCP port as the third runtime argument + ctx.emitter.instruction("sub sp, sp, #16"); // scratch frame the crypto-on-connect helper reads + ctx.emitter.instruction("str x0, [sp, #0]"); // hostname pointer, which carries any crypto scheme + ctx.emitter.instruction("str x1, [sp, #8]"); // hostname byte length } Arch::X86_64 => { load_string_to_result(ctx, host, "fsockopen host")?; @@ -3669,9 +4430,50 @@ pub(crate) fn lower_fsockopen(ctx: &mut FunctionContext<'_>, inst: &Instruction) abi::emit_pop_reg(ctx.emitter, "r8"); abi::emit_pop_reg_pair(ctx.emitter, "rdi", "rsi"); ctx.emitter.instruction("mov rdx, r8"); // pass TCP port as the third runtime argument + ctx.emitter.instruction("sub rsp, 16"); // scratch frame the crypto-on-connect helper reads + ctx.emitter.instruction("mov QWORD PTR [rsp + 0], rdi"); // hostname pointer, which carries any crypto scheme + ctx.emitter.instruction("mov QWORD PTR [rsp + 8], rsi"); // hostname byte length } } abi::emit_call_label(ctx.emitter, "__rt_fsockopen"); + // php accepts a crypto scheme in the hostname and hands back an encrypted + // stream, because fsockopen and stream_socket_client share one transport + // factory. The connect happens inside __rt_fsockopen, so the handshake is + // driven here, on the hostname the caller passed. + let may_negotiate = address_may_select_tls(ctx, host); + if may_negotiate { + // rustls needs a server name to build the client session, and the crypto + // helper reads it from the per-descriptor connect-host table. The + // stream_socket_client lowering fills that table itself; fsockopen reaches + // the socket through __rt_fsockopen, so it must record the hostname here or + // the attach fails and the connect reports false. + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("ldr x1, [sp, #0]"); // hostname pointer + ctx.emitter.instruction("ldr x2, [sp, #8]"); // hostname byte length + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rdi, rax"); // connected descriptor + ctx.emitter.instruction("mov rsi, QWORD PTR [rsp + 0]"); // hostname pointer + ctx.emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // hostname byte length + } + } + abi::emit_call_label(ctx.emitter, "__rt_stash_connect_host"); + } + match ctx.emitter.target.arch { + Arch::AArch64 => { + if may_negotiate { + emit_stream_client_crypto_on_connect_aarch64(ctx); + } + ctx.emitter.instruction("add sp, sp, #16"); // release the hostname scratch frame + } + Arch::X86_64 => { + if may_negotiate { + emit_stream_client_crypto_on_connect_x86_64(ctx); + } + ctx.emitter.instruction("add rsp, 16"); // release the hostname scratch frame + } + } store_fsockopen_error_outputs(ctx, inst)?; box_stream_fd_or_false_result(ctx, "fsockopen"); store_if_result(ctx, inst) @@ -3838,7 +4640,7 @@ pub(crate) fn lower_elephc_phar_set_compression( ); ctx.emitter.instruction("test r10, r10"); // test whether the PHAR compression bridge was published ctx.emitter.instruction(&format!("jz {}", fail)); // missing bridge makes compression control fail - ctx.emitter.instruction("call r10"); // rewrite native-PHAR entry compression flags + ctx.emitter.emit_native_bridge_call("r10", 3); // rewrite native-PHAR entry compression flags through the platform C ABI ctx.emitter.instruction("test rax, rax"); // test the bridge success flag ctx.emitter.instruction("setne al"); // normalize bridge result to PHP bool ctx.emitter.instruction("movzx eax, al"); // widen the normalized bool @@ -3942,7 +4744,7 @@ fn emit_phar_set_string_bridge( abi::emit_load_symbol_to_reg(ctx.emitter, "r10", slot, 0); ctx.emitter.instruction("test r10, r10"); // test whether the PHAR write bridge was published ctx.emitter.instruction(&format!("jz {}", fail)); // missing bridge makes the write fail - ctx.emitter.instruction("call r10"); // rewrite the archive with the new metadata/stub + ctx.emitter.emit_native_bridge_call("r10", 4); // rewrite the archive through the platform C ABI ctx.emitter.instruction("test rax, rax"); // test the bridge success flag ctx.emitter.instruction("setne al"); // normalize bridge result to PHP bool ctx.emitter.instruction("movzx eax, al"); // widen the normalized bool @@ -3993,7 +4795,7 @@ fn emit_phar_string_to_bool_bridge( abi::emit_load_symbol_to_reg(ctx.emitter, "r10", slot, 0); ctx.emitter.instruction("test r10, r10"); // test whether the bridge was published ctx.emitter.instruction(&format!("jz {}", fail)); // missing bridge yields false - ctx.emitter.instruction("call r10"); // call the bridge setter + ctx.emitter.emit_native_bridge_call("r10", 2); // call the bridge setter through the platform C ABI ctx.emitter.instruction("test rax, rax"); // test the bridge return flag ctx.emitter.instruction("setne al"); // normalize to a PHP bool ctx.emitter.instruction("movzx eax, al"); // widen the normalized bool @@ -4050,7 +4852,7 @@ fn emit_phar_get_string_bridge( abi::emit_load_symbol_to_reg(ctx.emitter, "r10", slot, 0); ctx.emitter.instruction("test r10, r10"); // test whether the PHAR read bridge was published ctx.emitter.instruction(&format!("jz {}", empty)); // missing bridge yields an empty string - ctx.emitter.instruction("call r10"); // read the metadata/stub bytes into the global buffer + ctx.emitter.emit_native_bridge_call("r10", 3); // read metadata/stub bytes through the platform C ABI ctx.emitter.instruction("test rax, rax"); // a null result means the field is unset ctx.emitter.instruction(&format!("jz {}", empty)); // fall back to an empty string ctx.emitter.instruction("mov rdi, rax"); // str_persist source pointer = bridge buffer @@ -4256,7 +5058,7 @@ fn emit_phar_path_int_to_bool_bridge( abi::emit_load_symbol_to_reg(ctx.emitter, "r10", slot, 0); ctx.emitter.instruction("test r10, r10"); // test whether the bridge was published ctx.emitter.instruction(&format!("jz {}", fail)); // missing bridge makes the op fail - ctx.emitter.instruction("call r10"); // invoke the bridge + ctx.emitter.emit_native_bridge_call("r10", 3); // invoke the bridge through the platform C ABI ctx.emitter.instruction("test rax, rax"); // test the bridge success flag ctx.emitter.instruction("setne al"); // normalize to PHP bool ctx.emitter.instruction("movzx eax, al"); // widen the normalized bool @@ -4305,7 +5107,7 @@ pub(crate) fn lower_elephc_phar_list_entries( ); ctx.emitter.instruction("test r10, r10"); // test whether the PHAR list bridge was published ctx.emitter.instruction(&format!("jz {}", empty)); // missing bridge yields an empty entry list - ctx.emitter.instruction("call r10"); // serialize archive entry names into the bridge buffer + ctx.emitter.emit_native_bridge_call("r10", 3); // serialize archive entry names through the platform C ABI ctx.emitter.instruction("test rax, rax"); // test whether the bridge returned a serialized buffer ctx.emitter.instruction(&format!("jz {}", empty)); // unreadable archives yield an empty entry list emit_phar_list_entries_buffer_to_array_x86_64(ctx); @@ -4447,7 +5249,30 @@ pub(crate) fn lower_rename(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> /// Lowers `tempnam(directory, prefix)` through the target-aware runtime helper. pub(crate) fn lower_tempnam(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - lower_binary_path_call(ctx, inst, "tempnam", "__rt_tempnam") + super::ensure_arg_count(inst, "tempnam", 2)?; + let directory = expect_operand(inst, 0)?; + let prefix = expect_operand(inst, 1)?; + match ctx.emitter.target.arch { + Arch::AArch64 => { + load_string_to_result(ctx, directory, "tempnam")?; + abi::emit_push_reg_pair(ctx.emitter, "x1", "x2"); + load_string_to_result(ctx, prefix, "tempnam")?; + ctx.emitter.instruction("mov x3, x1"); // pass the prefix pointer in the runtime helper's secondary string slot + ctx.emitter.instruction("mov x4, x2"); // pass the prefix length in the runtime helper's secondary string slot + abi::emit_pop_reg_pair(ctx.emitter, "x1", "x2"); + } + Arch::X86_64 => { + load_string_to_result(ctx, directory, "tempnam")?; + abi::emit_push_reg_pair(ctx.emitter, "rax", "rdx"); + load_string_to_result(ctx, prefix, "tempnam")?; + ctx.emitter.instruction("mov rdi, rax"); // pass the prefix pointer while the directory remains on the stack + ctx.emitter.instruction("mov rsi, rdx"); // pass the prefix length while the directory remains on the stack + abi::emit_pop_reg_pair(ctx.emitter, "rax", "rdx"); + } + } + abi::emit_call_label(ctx.emitter, "__rt_tempnam"); + box_owned_string_or_false_result(ctx, "tempnam"); + store_if_result(ctx, inst) } /// Lowers `scandir(path)` through the target-aware runtime directory listing helper. @@ -5395,16 +6220,26 @@ pub(crate) fn lower_getcwd(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> store_if_result(ctx, inst) } -/// Lowers `sys_get_temp_dir()` as the project's hardcoded `/tmp` string. +/// Lowers `sys_get_temp_dir()`. On Windows, calls the `__rt_sys_get_temp_dir` +/// runtime helper (`GetTempPathW`-backed); on every other target, emits the +/// project's hardcoded `/tmp` string, unchanged. pub(crate) fn lower_sys_get_temp_dir( ctx: &mut FunctionContext<'_>, inst: &Instruction, ) -> Result<()> { super::ensure_arg_count(inst, "sys_get_temp_dir", 0)?; - let (label, len) = ctx.data.add_string(b"/tmp"); - let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); - abi::emit_symbol_address(ctx.emitter, ptr_reg, &label); - abi::emit_load_int_immediate(ctx.emitter, len_reg, len as i64); + if ctx.emitter.target.platform == Platform::Windows { + // __rt_sys_get_temp_dir already returns its owned string in + // abi::string_result_regs (rax/rdx on x86_64) — no register shuffle needed. + abi::emit_call_label(ctx.emitter, "__rt_sys_get_temp_dir"); + return store_if_result(ctx, inst); + } + + // php resolves the directory once, in php_get_temporary_directory: TMPDIR + // first, minus exactly one trailing slash, then P_tmpdir verbatim. tmpfile() + // reads it through the same helper, so the two can never disagree about where + // temporary files belong. + abi::emit_call_label(ctx.emitter, "__rt_php_temp_dir"); store_if_result(ctx, inst) } @@ -5429,7 +6264,7 @@ pub(crate) fn lower_filemtime( ctx: &mut FunctionContext<'_>, inst: &Instruction, ) -> Result<()> { - lower_unary_path_int(ctx, inst, "filemtime", "__rt_filemtime") + lower_unary_path_stat_int_or_false(ctx, inst, "filemtime", "__rt_filemtime") } /// Lowers `linkinfo(path)` through the target-aware runtime lstat helper. @@ -5763,6 +6598,7 @@ fn lower_filesize_with_wrapper(ctx: &mut FunctionContext<'_>, inst: &Instruction let path = expect_operand(inst, 0)?; load_string_to_result(ctx, path, "filesize")?; emit_url_stat_field_or_fallback(ctx, "__rt_filesize", 0); + box_stat_int_or_false_result(ctx); store_if_result(ctx, inst) } @@ -5795,6 +6631,7 @@ fn emit_url_stat_field_or_fallback( abi::emit_symbol_address(ctx.emitter, "x9", "_url_stat_matched"); ctx.emitter.instruction("ldrb w9, [x9]"); // read whether a registered wrapper scheme matched ctx.emitter.instruction(&format!("cbz w9, {}", fallback)); // fall back to filesystem stat when no wrapper matched + ctx.emitter.instruction("mov x1, #1"); // a matched wrapper is a successful stat for int|false consumers ctx.emitter.instruction(&format!("b {}", done)); // keep the wrapper field result ctx.emitter.label(&fallback); ctx.emitter.instruction("ldr x1, [sp, #0]"); // restore the path pointer for filesystem fallback @@ -5815,6 +6652,7 @@ fn emit_url_stat_field_or_fallback( ctx.emitter.instruction("movzx r9d, BYTE PTR [r9]"); // read whether a registered wrapper scheme matched ctx.emitter.instruction("test r9d, r9d"); // test the url_stat matched flag ctx.emitter.instruction(&format!("jz {}", fallback)); // fall back to filesystem stat when no wrapper matched + ctx.emitter.instruction("mov rdx, 1"); // a matched wrapper is a successful stat for int|false consumers ctx.emitter.instruction(&format!("jmp {}", done)); // keep the wrapper field result ctx.emitter.label(&fallback); ctx.emitter.instruction("mov rax, QWORD PTR [rsp + 0]"); // restore the path pointer for filesystem fallback @@ -6009,7 +6847,7 @@ fn emit_unlink_maybe_phar_dispatch(ctx: &mut FunctionContext<'_>) { ctx.emitter.instruction(&format!("jz {}", phar_fail)); // missing bridge makes PHAR unlink fail ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 0]"); // bridge arg 0 = full phar:// URL pointer ctx.emitter.instruction("mov rsi, QWORD PTR [rsp + 8]"); // bridge arg 1 = full phar:// URL length - ctx.emitter.instruction("call r10"); // delete the archive entry through elephc-phar + ctx.emitter.emit_native_bridge_call("r10", 2); // delete the archive entry through the platform C ABI ctx.emitter.instruction("test rax, rax"); // test the bridge success flag ctx.emitter.instruction("setne al"); // normalize bridge result to PHP bool ctx.emitter.instruction("movzx eax, al"); // widen the normalized bool @@ -6184,7 +7022,7 @@ fn source_load_local_slot( let Some(inst_ref) = ctx.function.instruction(inst) else { return Err(CodegenIrError::missing_entry("instruction", inst.as_raw())); }; - if inst_ref.op == Op::LoadLocal { + if matches!(inst_ref.op, Op::LoadLocal | Op::LoadRefCell) { if let Some(Immediate::LocalSlot(slot)) = inst_ref.immediate { return Ok(Some(slot)); } @@ -6599,7 +7437,16 @@ fn lower_builtin_stream_filter_attach( ) -> Result<()> { let stream = expect_operand(inst, 0)?; load_stream_fd_to_result(ctx, stream, "stream_filter_append")?; - abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + match (ctx.emitter.target.arch, ctx.emitter.target.platform) { + (Arch::AArch64, _) => abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)), + (Arch::X86_64, Platform::Windows) => { + abi::emit_push_reg(ctx.emitter, "rax"); // preserve the real descriptor for the returned filter resource + ctx.emitter.instruction("mov rdi, rax"); // pass the opaque descriptor to the bounded stream-state registry + abi::emit_call_label(ctx.emitter, "__rt_win_stream_slot"); + abi::emit_push_reg(ctx.emitter, "rax"); // retain the compact filter-table slot across mode evaluation + } + (Arch::X86_64, _) => abi::emit_push_reg(ctx.emitter, "rax"), + } materialize_stream_filter_mode(ctx, inst)?; let skip_read = ctx.next_label("sf_skip_read"); let skip_write = ctx.next_label("sf_skip_write"); @@ -6632,7 +7479,11 @@ fn lower_builtin_stream_filter_attach( abi::emit_symbol_address(ctx.emitter, "r9", "_stream_write_filters"); // write-filter table base ctx.emitter.instruction(&format!("mov BYTE PTR [r9 + rcx], {}", id)); // record the write filter for this descriptor ctx.emitter.label(&skip_write); - ctx.emitter.instruction("mov rax, rcx"); // move the descriptor into the resource payload register + if ctx.emitter.target.platform == Platform::Windows { + abi::emit_pop_reg(ctx.emitter, "rax"); // restore the real descriptor after compact-slot table writes + } else { + ctx.emitter.instruction("mov rax, rcx"); // move the descriptor into the resource payload register + } } } emit_boxed_stream_resource(ctx); @@ -6684,7 +7535,21 @@ fn lower_user_stream_filter_attach( let stream = expect_operand(inst, 0)?; let filter = expect_operand(inst, 1)?; load_stream_fd_to_result(ctx, stream, "stream_filter_append")?; - abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + let raw_socket_label = ctx.next_label("sfau_raw_socket"); + match (ctx.emitter.target.arch, ctx.emitter.target.platform) { + (Arch::AArch64, _) => abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)), + (Arch::X86_64, Platform::Windows) => { + abi::emit_push_reg(ctx.emitter, "rax"); + ctx.emitter.instruction("mov rdi, rax"); // probe the opaque stream before user-filter registry indexing + abi::emit_call_label(ctx.emitter, "__rt_win_is_crt_fd"); + ctx.emitter.instruction("mov r8, rax"); // preserve predicate while restoring the descriptor + abi::emit_pop_reg(ctx.emitter, "rax"); + ctx.emitter.instruction("test r8, r8"); // raw Winsock SOCKETs are not CRT fd-table keys + ctx.emitter.instruction(&format!("jz {}", raw_socket_label)); // return PHP false instead of indexing out of bounds + abi::emit_push_reg(ctx.emitter, "rax"); + } + (Arch::X86_64, _) => abi::emit_push_reg(ctx.emitter, "rax"), + } load_string_to_result(ctx, filter, "stream_filter_append filter")?; match ctx.emitter.target.arch { Arch::AArch64 => { @@ -6733,6 +7598,9 @@ fn lower_user_stream_filter_attach( ctx.emitter.label(&fail); abi::emit_release_temporary_stack(ctx.emitter, 16); emit_boxed_bool(ctx, false); + ctx.emitter.instruction(&format!("jmp {}", done)); // do not fall through to raw-socket rejection + ctx.emitter.label(&raw_socket_label); + emit_boxed_bool(ctx, false); ctx.emitter.label(&done); } } @@ -6990,33 +7858,34 @@ fn lower_stream_bucket_append_x86_64( /// Tears down the TLS session attached to the current fd result, if one exists. fn emit_tls_session_teardown_for_current_fd(ctx: &mut FunctionContext<'_>) { let skip = ctx.next_label("tls_teardown_skip"); + let no_session = ctx.next_label("tls_teardown_none"); match ctx.emitter.target.arch { Arch::AArch64 => { - abi::emit_symbol_address(ctx.emitter, "x9", "_tls_sessions"); - ctx.emitter.instruction("ldr x10, [x9, x0, lsl #3]"); // load the TLS session handle for this descriptor - ctx.emitter.instruction(&format!("cbz x10, {}", skip)); // skip close_notify when no TLS session is attached abi::emit_push_reg(ctx.emitter, "x0"); - ctx.emitter.instruction("mov x0, x10"); // pass the TLS handle to the close helper + abi::emit_call_label(ctx.emitter, "__rt_tls_session_clear"); + ctx.emitter.instruction(&format!("cbz x0, {}", no_session)); // skip close_notify when no TLS session is attached abi::emit_symbol_address(ctx.emitter, "x9", "_elephc_tls_close_fn"); ctx.emitter.instruction("ldr x9, [x9]"); // load the published TLS close function pointer - ctx.emitter.instruction("blr x9"); // close the TLS session and send close_notify + ctx.emitter.emit_published_bridge_call("x9"); // close the TLS session through the published ABI entry + abi::emit_pop_reg(ctx.emitter, "x0"); + ctx.emitter.instruction(&format!("b {}", skip)); // descriptor is restored after closing the TLS session + ctx.emitter.label(&no_session); abi::emit_pop_reg(ctx.emitter, "x0"); - abi::emit_symbol_address(ctx.emitter, "x9", "_tls_sessions"); - ctx.emitter.instruction("str xzr, [x9, x0, lsl #3]"); // clear the per-fd TLS session slot ctx.emitter.label(&skip); } Arch::X86_64 => { - abi::emit_symbol_address(ctx.emitter, "r9", "_tls_sessions"); // TLS session table base - ctx.emitter.instruction("mov r10, QWORD PTR [r9 + rax*8]"); // load the TLS session handle for this descriptor - ctx.emitter.instruction("test r10, r10"); // test whether a TLS session is attached - ctx.emitter.instruction(&format!("je {}", skip)); // skip close_notify when no TLS session is attached abi::emit_push_reg(ctx.emitter, "rax"); - ctx.emitter.instruction("mov rdi, r10"); // pass the TLS handle to the close helper + ctx.emitter.instruction("mov rdi, rax"); // pass the full-width descriptor to the bounded map clear + abi::emit_call_label(ctx.emitter, "__rt_tls_session_clear"); + ctx.emitter.instruction("test rax, rax"); // test whether a TLS session was attached + ctx.emitter.instruction(&format!("je {}", no_session)); // skip close_notify when no TLS session is attached + ctx.emitter.instruction("mov rdi, rax"); // pass the removed TLS handle to the close helper abi::emit_load_symbol_to_reg(ctx.emitter, "r9", "_elephc_tls_close_fn", 0); // load the published TLS close function pointer - ctx.emitter.instruction("call r9"); // close the TLS session and send close_notify + ctx.emitter.emit_published_bridge_call("r9"); // close the TLS session through the published ABI entry + abi::emit_pop_reg(ctx.emitter, "rax"); + ctx.emitter.instruction(&format!("jmp {}", skip)); // descriptor is restored after closing the TLS session + ctx.emitter.label(&no_session); abi::emit_pop_reg(ctx.emitter, "rax"); - abi::emit_symbol_address(ctx.emitter, "r9", "_tls_sessions"); // TLS session table base - ctx.emitter.instruction("mov QWORD PTR [r9 + rax*8], 0"); // clear the per-fd TLS session slot ctx.emitter.label(&skip); } } @@ -7038,8 +7907,19 @@ fn emit_zlib_flush_on_close_for_current_fd(ctx: &mut FunctionContext<'_>) { ctx.emitter.label(&skip); } Arch::X86_64 => { + if ctx.emitter.target.platform == Platform::Windows { + abi::emit_push_reg(ctx.emitter, "rax"); + ctx.emitter.instruction("mov rdi, rax"); // pass raw Windows descriptor to the bounded stream-slot registry + ctx.emitter.instruction("call __rt_win_stream_slot"); // map opaque SOCKET/CRT descriptor before table access + ctx.emitter.instruction("mov r11, rax"); // retain compact slot for zlib-state lookup + abi::emit_pop_reg(ctx.emitter, "rax"); + } abi::emit_symbol_address(ctx.emitter, "r9", "_zstream_handles"); // zlib stream handle table base - ctx.emitter.instruction("mov r10, QWORD PTR [r9 + rax*8]"); // load this descriptor's zlib stream handle + if ctx.emitter.target.platform == Platform::Windows { + ctx.emitter.instruction("mov r10, QWORD PTR [r9 + r11*8]"); // load zlib state through the bounded Windows slot + } else { + ctx.emitter.instruction("mov r10, QWORD PTR [r9 + rax*8]"); // preserve established Linux descriptor indexing + } ctx.emitter.instruction("test r10, r10"); // test whether a zlib filter is attached ctx.emitter.instruction(&format!("je {}", skip)); // skip flush when no zlib filter is attached abi::emit_push_reg(ctx.emitter, "rax"); @@ -7068,8 +7948,19 @@ fn emit_bz2_flush_on_close_for_current_fd(ctx: &mut FunctionContext<'_>) { ctx.emitter.label(&skip); } Arch::X86_64 => { + if ctx.emitter.target.platform == Platform::Windows { + abi::emit_push_reg(ctx.emitter, "rax"); + ctx.emitter.instruction("mov rdi, rax"); // pass raw Windows descriptor to the bounded stream-slot registry + ctx.emitter.instruction("call __rt_win_stream_slot"); // map opaque SOCKET/CRT descriptor before table access + ctx.emitter.instruction("mov r11, rax"); // retain compact slot for bzip2-state lookup + abi::emit_pop_reg(ctx.emitter, "rax"); + } abi::emit_symbol_address(ctx.emitter, "r9", "_bzstream_handles"); // bzip2 stream handle table base - ctx.emitter.instruction("mov r10, QWORD PTR [r9 + rax*8]"); // load this descriptor's bzip2 stream handle + if ctx.emitter.target.platform == Platform::Windows { + ctx.emitter.instruction("mov r10, QWORD PTR [r9 + r11*8]"); // load bzip2 state through the bounded Windows slot + } else { + ctx.emitter.instruction("mov r10, QWORD PTR [r9 + rax*8]"); // preserve established Linux descriptor indexing + } ctx.emitter.instruction("test r10, r10"); // test whether a bzip2 filter is attached ctx.emitter.instruction(&format!("je {}", skip)); // skip flush when no bzip2 filter is attached abi::emit_push_reg(ctx.emitter, "rax"); @@ -7098,8 +7989,19 @@ fn emit_iconv_flush_on_close_for_current_fd(ctx: &mut FunctionContext<'_>) { ctx.emitter.label(&skip); } Arch::X86_64 => { + if ctx.emitter.target.platform == Platform::Windows { + abi::emit_push_reg(ctx.emitter, "rax"); + ctx.emitter.instruction("mov rdi, rax"); // pass raw Windows descriptor to the bounded stream-slot registry + ctx.emitter.instruction("call __rt_win_stream_slot"); // map opaque SOCKET/CRT descriptor before table access + ctx.emitter.instruction("mov r11, rax"); // retain compact slot for iconv-state lookup + abi::emit_pop_reg(ctx.emitter, "rax"); + } abi::emit_symbol_address(ctx.emitter, "r9", "_iconv_handles"); // iconv transcoder handle table base - ctx.emitter.instruction("mov r10, QWORD PTR [r9 + rax*8]"); // load this descriptor's iconv transcoder handle + if ctx.emitter.target.platform == Platform::Windows { + ctx.emitter.instruction("mov r10, QWORD PTR [r9 + r11*8]"); // load iconv state through the bounded Windows slot + } else { + ctx.emitter.instruction("mov r10, QWORD PTR [r9 + rax*8]"); // preserve established Linux descriptor indexing + } ctx.emitter.instruction("test r10, r10"); // test whether an iconv write filter is attached ctx.emitter.instruction(&format!("je {}", skip)); // skip close when no iconv write filter is attached abi::emit_push_reg(ctx.emitter, "rax"); @@ -7112,85 +8014,272 @@ fn emit_iconv_flush_on_close_for_current_fd(ctx: &mut FunctionContext<'_>) { } } -/// Emits the AArch64 TLS attach path for `stream_socket_enable_crypto(true)`. -fn lower_stream_socket_enable_crypto_attach_aarch64( +/// Reads one string-valued SSL context option into an AArch64 TLS options field. +fn emit_tls_string_option_aarch64( ctx: &mut FunctionContext<'_>, - done_label: &str, + key_symbol: &str, + key_len: usize, + ptr_offset: usize, + len_offset: usize, ) { - crate::codegen::tls::publish_tls_function_pointers(ctx.emitter); - let fail_label = ctx.next_label("ssec_attach_fail"); + abi::emit_symbol_address(ctx.emitter, "x0", "_ssl_key_str"); + ctx.emitter.instruction("mov x1, #3"); // pass strlen("ssl") to the context lookup + abi::emit_symbol_address(ctx.emitter, "x2", key_symbol); + ctx.emitter.instruction(&format!("mov x3, #{}", key_len)); // pass the SSL option-name length + ctx.emitter.instruction(&format!("add x4, sp, #{}", ptr_offset)); // pass the options pointer-field address + ctx.emitter.instruction(&format!("add x5, sp, #{}", len_offset)); // pass the options length-field address + abi::emit_call_label(ctx.emitter, "__rt_get_string_context_option"); +} + +/// Reads one boolean SSL context option into AArch64 scratch storage. +fn emit_tls_bool_option_aarch64( + ctx: &mut FunctionContext<'_>, + key_symbol: &str, + key_len: usize, +) { + abi::emit_symbol_address(ctx.emitter, "x0", "_ssl_key_str"); + ctx.emitter.instruction("mov x1, #3"); // pass strlen("ssl") to the context lookup + abi::emit_symbol_address(ctx.emitter, "x2", key_symbol); + ctx.emitter.instruction(&format!("mov x3, #{}", key_len)); // pass the SSL option-name length + ctx.emitter.instruction("add x4, sp, #104"); // pass the reusable boolean scratch address + abi::emit_call_label(ctx.emitter, "__rt_get_bool_context_option"); +} + +/// Builds PHP-compatible TLS client options for AArch64 STARTTLS attachment. +fn emit_stream_crypto_tls_options_aarch64(ctx: &mut FunctionContext<'_>) { let peer_ok = ctx.next_label("ssec_peer_ok"); - let host_default = ctx.next_label("ssec_host_default"); - let plain_attach = ctx.next_label("ssec_plain_attach"); - let do_attach = ctx.next_label("ssec_do_attach"); - ctx.emitter.instruction("sub sp, sp, #64"); // reserve peer-name and client-cert/key spill storage - ctx.emitter.instruction("add x0, sp, #0"); // pass peer-name out_ptr address - ctx.emitter.instruction("add x1, sp, #8"); // pass peer-name out_len address + let verify_peer_done = ctx.next_label("ssec_verify_peer_done"); + let verify_name_done = ctx.next_label("ssec_verify_name_done"); + let allow_self_signed_done = ctx.next_label("ssec_allow_self_signed_done"); + let crypto_method_done = ctx.next_label("ssec_crypto_method_done"); + + ctx.emitter.instruction("mov w9, #1"); // options ABI version one + ctx.emitter.instruction("str w9, [sp, #0]"); // initialize the options ABI version + ctx.emitter.instruction("mov w9, #3"); // default to peer and peer-name verification + ctx.emitter.instruction("str w9, [sp, #4]"); // initialize the independent TLS policy bits + for offset in [8usize, 24, 40, 56, 72] { + ctx.emitter.instruction(&format!("stp xzr, xzr, [sp, #{}]", offset)); // initialize one optional pointer/length pair + } + + ctx.emitter.instruction("add x0, sp, #8"); // pass peer-name pointer-field address + ctx.emitter.instruction("add x1, sp, #16"); // pass peer-name length-field address abi::emit_call_label(ctx.emitter, "__rt_get_ssl_peer_name"); - ctx.emitter.instruction(&format!("cbnz x0, {}", peer_ok)); // use ssl.peer_name when the context provides it - ctx.emitter.instruction("ldr x10, [sp, #64]"); // reload fd for the connect-host table lookup - abi::emit_symbol_address(ctx.emitter, "x9", "_stream_connect_host"); - ctx.emitter.instruction("add x9, x9, x10, lsl #4"); // address this fd's saved host pointer/length pair - ctx.emitter.instruction("ldr x11, [x9, #8]"); // load the saved connection-host byte length - ctx.emitter.instruction(&format!("cbz x11, {}", host_default)); // fall back to localhost when no connection host is known - ctx.emitter.instruction("ldr x12, [x9, #0]"); // load the saved connection-host pointer - ctx.emitter.instruction("str x12, [sp, #0]"); // use the connection host as peer_name pointer - ctx.emitter.instruction("str x11, [sp, #8]"); // use the connection host as peer_name length - ctx.emitter.instruction(&format!("b {}", peer_ok)); // skip the localhost fallback - ctx.emitter.label(&host_default); - abi::emit_symbol_address(ctx.emitter, "x9", "_tls_peer_name_default"); - ctx.emitter.instruction("str x9, [sp, #0]"); // use localhost as the fallback peer_name pointer - ctx.emitter.instruction("mov x9, #9"); // strlen("localhost") - ctx.emitter.instruction("str x9, [sp, #8]"); // use localhost as the fallback peer_name length + ctx.emitter.instruction(&format!("cbnz x0, {}", peer_ok)); // use an explicit ssl.peer_name when present + ctx.emitter.instruction("ldr x0, [sp, #112]"); // reload the full-width descriptor for host metadata lookup + abi::emit_call_label(ctx.emitter, "__rt_get_stashed_connect_host"); + ctx.emitter.instruction(&format!("cbz x0, {}", peer_ok)); // leave the peer name absent when no URL host is known + ctx.emitter.instruction("str x1, [sp, #8]"); // use the stashed host as the TLS peer name + ctx.emitter.instruction("str x2, [sp, #16]"); // store the stashed host byte length ctx.emitter.label(&peer_ok); - ctx.emitter.instruction("str xzr, [sp, #24]"); // default local_cert length to zero - ctx.emitter.instruction("str xzr, [sp, #40]"); // default local_pk length to zero + emit_tls_string_option_aarch64(ctx, "_ssl_cafile_key_str", 6, 24, 32); + emit_tls_string_option_aarch64(ctx, "_ssl_capath_key_str", 6, 40, 48); + emit_tls_string_option_aarch64(ctx, "_ssl_local_cert_key_str", 10, 56, 64); + emit_tls_string_option_aarch64(ctx, "_ssl_local_pk_key_str", 8, 72, 80); + + ctx.emitter.instruction("mov x9, #1"); // verify_peer defaults to true + ctx.emitter.instruction("str x9, [sp, #104]"); // seed the boolean lookup default + emit_tls_bool_option_aarch64(ctx, "_ssl_verify_peer_key_str", 11); + ctx.emitter.instruction("ldr x9, [sp, #104]"); // load the resolved verify_peer truth value + ctx.emitter.instruction(&format!("cbnz x9, {}", verify_peer_done)); // retain peer verification when truthy + ctx.emitter.instruction("ldr w10, [sp, #4]"); // load the TLS policy bitmask + ctx.emitter.instruction("bic w10, w10, #1"); // disable only certificate-chain verification + ctx.emitter.instruction("str w10, [sp, #4]"); // store the updated TLS policy + ctx.emitter.label(&verify_peer_done); + + ctx.emitter.instruction("mov x9, #1"); // verify_peer_name defaults to true + ctx.emitter.instruction("str x9, [sp, #104]"); // seed the boolean lookup default + emit_tls_bool_option_aarch64(ctx, "_ssl_verify_peer_name_key_str", 16); + ctx.emitter.instruction("ldr x9, [sp, #104]"); // load the resolved verify_peer_name truth value + ctx.emitter.instruction(&format!("cbnz x9, {}", verify_name_done)); // retain peer-name verification when truthy + ctx.emitter.instruction("ldr w10, [sp, #4]"); // load the TLS policy bitmask + ctx.emitter.instruction("bic w10, w10, #2"); // disable only peer-name verification + ctx.emitter.instruction("str w10, [sp, #4]"); // store the updated TLS policy + ctx.emitter.label(&verify_name_done); + + ctx.emitter.instruction("str xzr, [sp, #104]"); // allow_self_signed defaults to false + emit_tls_bool_option_aarch64(ctx, "_ssl_allow_self_signed_key_str", 17); + ctx.emitter.instruction("ldr x9, [sp, #104]"); // load the resolved allow_self_signed truth value + ctx.emitter.instruction(&format!("cbz x9, {}", allow_self_signed_done)); // leave the exception disabled when false + ctx.emitter.instruction("ldr w10, [sp, #4]"); // load the TLS policy bitmask + ctx.emitter.instruction("orr w10, w10, #4"); // permit only a depth-zero self-signed leaf + ctx.emitter.instruction("str w10, [sp, #4]"); // store the updated TLS policy + ctx.emitter.label(&allow_self_signed_done); + + ctx.emitter.instruction("ldr x9, [sp, #128]"); // load the caller's crypto-method presence flag + ctx.emitter.instruction(&format!("cbnz x9, {}", crypto_method_done)); // explicit argument takes precedence over context abi::emit_symbol_address(ctx.emitter, "x0", "_ssl_key_str"); - ctx.emitter.instruction("mov x1, #3"); // strlen("ssl") - abi::emit_symbol_address(ctx.emitter, "x2", "_ssl_local_cert_key_str"); - ctx.emitter.instruction("mov x3, #10"); // strlen("local_cert") - ctx.emitter.instruction("add x4, sp, #16"); // pass local_cert out_ptr address - ctx.emitter.instruction("add x5, sp, #24"); // pass local_cert out_len address - abi::emit_call_label(ctx.emitter, "__rt_get_string_context_option"); - abi::emit_symbol_address(ctx.emitter, "x0", "_ssl_key_str"); - ctx.emitter.instruction("mov x1, #3"); // strlen("ssl") - abi::emit_symbol_address(ctx.emitter, "x2", "_ssl_local_pk_key_str"); - ctx.emitter.instruction("mov x3, #8"); // strlen("local_pk") - ctx.emitter.instruction("add x4, sp, #32"); // pass local_pk out_ptr address - ctx.emitter.instruction("add x5, sp, #40"); // pass local_pk out_len address + ctx.emitter.instruction("mov x1, #3"); // pass strlen("ssl") to the context lookup + abi::emit_symbol_address(ctx.emitter, "x2", "_ssl_crypto_method_key_str"); + ctx.emitter.instruction("mov x3, #13"); // pass strlen("crypto_method") + ctx.emitter.instruction("add x4, sp, #120"); // write the resolved context crypto method + abi::emit_call_label(ctx.emitter, "__rt_get_int_context_option"); + ctx.emitter.instruction("str x0, [sp, #128]"); // retain whether ssl.crypto_method was found + ctx.emitter.label(&crypto_method_done); +} + +/// Reads one string-valued SSL context option into an x86_64 TLS options field. +fn emit_tls_string_option_x86_64( + ctx: &mut FunctionContext<'_>, + key_symbol: &str, + key_len: usize, + ptr_offset: usize, + len_offset: usize, +) { + abi::emit_symbol_address(ctx.emitter, "rdi", "_ssl_key_str"); + ctx.emitter.instruction("mov rsi, 3"); // pass strlen("ssl") to the context lookup + abi::emit_symbol_address(ctx.emitter, "rdx", key_symbol); + ctx.emitter.instruction(&format!("mov rcx, {}", key_len)); // pass the SSL option-name length + ctx.emitter.instruction(&format!("lea r8, [rsp + {}]", ptr_offset)); // pass the options pointer-field address + ctx.emitter.instruction(&format!("lea r9, [rsp + {}]", len_offset)); // pass the options length-field address abi::emit_call_label(ctx.emitter, "__rt_get_string_context_option"); +} - ctx.emitter.instruction("ldr x0, [sp, #64]"); // reload fd as the first TLS attach argument - ctx.emitter.instruction("ldr x1, [sp, #0]"); // pass peer_name pointer - ctx.emitter.instruction("ldr x2, [sp, #8]"); // pass peer_name byte length - ctx.emitter.instruction("ldr x9, [sp, #24]"); // load local_cert byte length - ctx.emitter.instruction(&format!("cbz x9, {}", plain_attach)); // no client certificate selects plain TLS attach - ctx.emitter.instruction("ldr x9, [sp, #40]"); // load local_pk byte length - ctx.emitter.instruction(&format!("cbz x9, {}", plain_attach)); // missing key selects plain TLS attach - ctx.emitter.instruction("ldr x3, [sp, #16]"); // pass local_cert path pointer - ctx.emitter.instruction("ldr x4, [sp, #24]"); // pass local_cert path length - ctx.emitter.instruction("ldr x5, [sp, #32]"); // pass local_pk path pointer - ctx.emitter.instruction("ldr x6, [sp, #40]"); // pass local_pk path length - abi::emit_symbol_address(ctx.emitter, "x9", "_elephc_tls_attach_fd_client_cert_fn"); - ctx.emitter.instruction("ldr x9, [x9]"); // load the mutual-TLS attach function pointer - ctx.emitter.instruction(&format!("b {}", do_attach)); // call the selected attach function - ctx.emitter.label(&plain_attach); - abi::emit_symbol_address(ctx.emitter, "x9", "_elephc_tls_attach_fd_fn"); - ctx.emitter.instruction("ldr x9, [x9]"); // load the default TLS attach function pointer - ctx.emitter.label(&do_attach); - ctx.emitter.instruction("blr x9"); // attach TLS to the fd and return a session handle - ctx.emitter.instruction("ldr x10, [sp, #64]"); // reload fd before releasing the spill storage - abi::emit_release_temporary_stack(ctx.emitter, 64); - abi::emit_release_temporary_stack(ctx.emitter, 16); +/// Reads one boolean SSL context option into x86_64 scratch storage. +fn emit_tls_bool_option_x86_64( + ctx: &mut FunctionContext<'_>, + key_symbol: &str, + key_len: usize, +) { + abi::emit_symbol_address(ctx.emitter, "rdi", "_ssl_key_str"); + ctx.emitter.instruction("mov rsi, 3"); // pass strlen("ssl") to the context lookup + abi::emit_symbol_address(ctx.emitter, "rdx", key_symbol); + ctx.emitter.instruction(&format!("mov rcx, {}", key_len)); // pass the SSL option-name length + ctx.emitter.instruction("lea r8, [rsp + 104]"); // pass the reusable boolean scratch address + abi::emit_call_label(ctx.emitter, "__rt_get_bool_context_option"); +} + +/// Builds PHP-compatible TLS client options for x86_64 STARTTLS attachment. +fn emit_stream_crypto_tls_options_x86_64(ctx: &mut FunctionContext<'_>) { + let peer_ok = ctx.next_label("ssec_peer_ok"); + let verify_peer_done = ctx.next_label("ssec_verify_peer_done"); + let verify_name_done = ctx.next_label("ssec_verify_name_done"); + let allow_self_signed_done = ctx.next_label("ssec_allow_self_signed_done"); + let crypto_method_done = ctx.next_label("ssec_crypto_method_done"); + + ctx.emitter.instruction("mov DWORD PTR [rsp + 0], 1"); // initialize options ABI version one + ctx.emitter.instruction("mov DWORD PTR [rsp + 4], 3"); // default to peer and peer-name verification + for offset in [8usize, 16, 24, 32, 40, 48, 56, 64, 72, 80] { + ctx.emitter.instruction(&format!("mov QWORD PTR [rsp + {}], 0", offset)); // initialize one optional options word + } + + ctx.emitter.instruction("lea rdi, [rsp + 8]"); // pass peer-name pointer-field address + ctx.emitter.instruction("lea rsi, [rsp + 16]"); // pass peer-name length-field address + abi::emit_call_label(ctx.emitter, "__rt_get_ssl_peer_name"); + ctx.emitter.instruction("test rax, rax"); // did the context provide ssl.peer_name? + ctx.emitter.instruction(&format!("jnz {}", peer_ok)); // use an explicit peer name when present + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 112]"); // reload the full-width descriptor for host metadata lookup + abi::emit_call_label(ctx.emitter, "__rt_get_stashed_connect_host"); + ctx.emitter.instruction("test rax, rax"); // is a connection host known for this descriptor? + ctx.emitter.instruction(&format!("jz {}", peer_ok)); // leave the peer name absent when no URL host is known + ctx.emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // use the stashed host as the TLS peer name + ctx.emitter.instruction("mov QWORD PTR [rsp + 16], rdx"); // store the stashed host byte length + ctx.emitter.label(&peer_ok); + + emit_tls_string_option_x86_64(ctx, "_ssl_cafile_key_str", 6, 24, 32); + emit_tls_string_option_x86_64(ctx, "_ssl_capath_key_str", 6, 40, 48); + emit_tls_string_option_x86_64(ctx, "_ssl_local_cert_key_str", 10, 56, 64); + emit_tls_string_option_x86_64(ctx, "_ssl_local_pk_key_str", 8, 72, 80); + + ctx.emitter.instruction("mov QWORD PTR [rsp + 104], 1"); // verify_peer defaults to true + emit_tls_bool_option_x86_64(ctx, "_ssl_verify_peer_key_str", 11); + ctx.emitter.instruction("cmp QWORD PTR [rsp + 104], 0"); // is peer verification enabled? + ctx.emitter.instruction(&format!("jne {}", verify_peer_done)); // retain peer verification when truthy + ctx.emitter.instruction("and DWORD PTR [rsp + 4], -2"); // disable only certificate-chain verification + ctx.emitter.label(&verify_peer_done); + + ctx.emitter.instruction("mov QWORD PTR [rsp + 104], 1"); // verify_peer_name defaults to true + emit_tls_bool_option_x86_64(ctx, "_ssl_verify_peer_name_key_str", 16); + ctx.emitter.instruction("cmp QWORD PTR [rsp + 104], 0"); // is peer-name verification enabled? + ctx.emitter.instruction(&format!("jne {}", verify_name_done)); // retain peer-name verification when truthy + ctx.emitter.instruction("and DWORD PTR [rsp + 4], -3"); // disable only peer-name verification + ctx.emitter.label(&verify_name_done); + + ctx.emitter.instruction("mov QWORD PTR [rsp + 104], 0"); // allow_self_signed defaults to false + emit_tls_bool_option_x86_64(ctx, "_ssl_allow_self_signed_key_str", 17); + ctx.emitter.instruction("cmp QWORD PTR [rsp + 104], 0"); // is the self-signed exception enabled? + ctx.emitter.instruction(&format!("je {}", allow_self_signed_done)); // leave the exception disabled when false + ctx.emitter.instruction("or DWORD PTR [rsp + 4], 4"); // permit only a depth-zero self-signed leaf + ctx.emitter.label(&allow_self_signed_done); + + ctx.emitter.instruction("cmp QWORD PTR [rsp + 128], 0"); // did the caller pass a crypto method? + ctx.emitter.instruction(&format!("jne {}", crypto_method_done)); // explicit argument takes precedence over context + abi::emit_symbol_address(ctx.emitter, "rdi", "_ssl_key_str"); + ctx.emitter.instruction("mov rsi, 3"); // pass strlen("ssl") to the context lookup + abi::emit_symbol_address(ctx.emitter, "rdx", "_ssl_crypto_method_key_str"); + ctx.emitter.instruction("mov rcx, 13"); // pass strlen("crypto_method") + ctx.emitter.instruction("lea r8, [rsp + 120]"); // write the resolved context crypto method + abi::emit_call_label(ctx.emitter, "__rt_get_int_context_option"); + ctx.emitter.instruction("mov QWORD PTR [rsp + 128], rax"); // retain whether ssl.crypto_method was found + ctx.emitter.label(&crypto_method_done); +} + +/// Emits the AArch64 TLS attach path for `stream_socket_enable_crypto(true)`. +fn lower_stream_socket_enable_crypto_attach_aarch64( + ctx: &mut FunctionContext<'_>, + done_label: &str, +) { + crate::codegen::tls::publish_tls_function_pointers(ctx.emitter); + let fail_label = ctx.next_label("ssec_attach_fail"); + let existing_session = ctx.next_label("ssec_existing_session"); + let handshake = ctx.next_label("ssec_handshake"); + let handshake_failed = ctx.next_label("ssec_handshake_failed"); + let release_success = ctx.next_label("ssec_release_success"); + let release_failure = ctx.next_label("ssec_release_failure"); + ctx.emitter.instruction("sub sp, sp, #112"); // reserve TLS options, session, status, and boolean scratch storage + ctx.emitter.instruction("ldr x0, [sp, #112]"); // look up any TLS session already associated with this descriptor + abi::emit_call_label(ctx.emitter, "__rt_tls_session_get"); + ctx.emitter.instruction(&format!("cbnz x0, {}", existing_session)); // resume an in-progress handshake instead of attaching twice + emit_stream_crypto_tls_options_aarch64(ctx); + ctx.emitter.instruction("ldr x0, [sp, #112]"); // pass the descriptor to the options-aware TLS attach bridge + ctx.emitter.instruction("mov x1, sp"); // pass the complete TLS client options structure + ctx.emitter.instruction("ldr x2, [sp, #120]"); // pass the requested PHP crypto method + ctx.emitter.instruction("ldr x3, [sp, #128]"); // distinguish explicit zero from an absent method + ctx.emitter.instruction("ldr x4, [sp, #136]"); // pass an optional source TLS session for resumption + abi::emit_symbol_address(ctx.emitter, "x9", "_elephc_tls_attach_fd_with_options_fn"); + ctx.emitter.instruction("ldr x9, [x9]"); // load the options-aware TLS attach function pointer + ctx.emitter.emit_published_bridge_call("x9"); // attach TLS with independent PHP verification policy ctx.emitter.instruction("cmp x0, #0"); // negative handles indicate TLS attach failure - ctx.emitter.instruction(&format!("b.lt {}", fail_label)); // return false when attach failed - abi::emit_symbol_address(ctx.emitter, "x11", "_tls_sessions"); - ctx.emitter.instruction("str x0, [x11, x10, lsl #3]"); // store the TLS session handle for this fd - ctx.emitter.instruction("mov x0, #1"); // return true after successful TLS attach + ctx.emitter.instruction(&format!("b.lt {}", fail_label)); // report attach failure after releasing storage + ctx.emitter.instruction("str x0, [sp, #88]"); // preserve the new session across table insertion + ctx.emitter.instruction("mov x1, x0"); // pass the new session as the bounded-map value + ctx.emitter.instruction("ldr x0, [sp, #112]"); // pass the full-width descriptor as the bounded-map key + abi::emit_call_label(ctx.emitter, "__rt_tls_session_set"); + ctx.emitter.instruction(&format!("cbz x0, {}", fail_label)); // table exhaustion closes the session and reports false + ctx.emitter.instruction(&format!("b {}", handshake)); // progress the newly attached TLS session + ctx.emitter.label(&existing_session); + ctx.emitter.instruction("str x0, [sp, #88]"); // preserve the existing TLS session across handshake + ctx.emitter.label(&handshake); + ctx.emitter.instruction("ldr x0, [sp, #88]"); // pass the persisted TLS session handle to rustls + abi::emit_symbol_address(ctx.emitter, "x9", "_elephc_tls_handshake_fn"); + ctx.emitter.instruction("ldr x9, [x9]"); // load the published TLS handshake entry pointer + ctx.emitter.emit_published_bridge_call("x9"); // complete or advance the TLS handshake through rustls + ctx.emitter.instruction("sxtw x0, w0"); // the bridge returns i32: bits 32-63 of x0 are undefined, so a + // negative status would read as a large positive 64-bit value + ctx.emitter.instruction("str x0, [sp, #96]"); // retain the PHP tri-state result across frame cleanup + ctx.emitter.instruction("cmp x0, #0"); // terminal handshake failure is negative + ctx.emitter.instruction(&format!("b.lt {}", handshake_failed)); // clear and close the failed TLS session + ctx.emitter.instruction(&format!("b {}", release_success)); // retain success or nonblocking progress + ctx.emitter.label(&release_success); + ctx.emitter.instruction("ldr x0, [sp, #96]"); // reload the retained handshake status + abi::emit_release_temporary_stack(ctx.emitter, 112); + abi::emit_release_temporary_stack(ctx.emitter, 32); ctx.emitter.instruction(&format!("b {}", done_label)); // skip the failure result + ctx.emitter.label(&handshake_failed); + ctx.emitter.instruction("ldr x0, [sp, #112]"); // clear the descriptor-to-session association + abi::emit_call_label(ctx.emitter, "__rt_tls_session_clear"); + ctx.emitter.instruction("ldr x0, [sp, #88]"); // close the failed rustls session + abi::emit_symbol_address(ctx.emitter, "x9", "_elephc_tls_close_fn"); + ctx.emitter.instruction("ldr x9, [x9]"); // load the published TLS close entry pointer + ctx.emitter.emit_published_bridge_call("x9"); // release the failed TLS session and duplicated socket + ctx.emitter.instruction("mov x0, #-1"); // normalize terminal TLS errors to PHP false + ctx.emitter.instruction(&format!("b {}", release_failure)); // release temporary storage before returning ctx.emitter.label(&fail_label); - ctx.emitter.instruction("mov x0, #0"); // return false after TLS attach failure + ctx.emitter.instruction("mov x0, #-1"); // return false after TLS attach or table failure + ctx.emitter.label(&release_failure); + abi::emit_release_temporary_stack(ctx.emitter, 112); + abi::emit_release_temporary_stack(ctx.emitter, 32); + ctx.emitter.instruction(&format!("b {}", done_label)); // normalize failures through PHP return boxing } /// Emits the x86_64 TLS attach path for `stream_socket_enable_crypto(true)`. @@ -7200,88 +8289,106 @@ fn lower_stream_socket_enable_crypto_attach_x86_64( ) { crate::codegen::tls::publish_tls_function_pointers(ctx.emitter); let fail_label = ctx.next_label("ssec_attach_fail"); - let peer_ok = ctx.next_label("ssec_peer_ok"); - let host_default = ctx.next_label("ssec_host_default"); - let plain_attach = ctx.next_label("ssec_plain_attach_x"); - let after_attach = ctx.next_label("ssec_after_attach_x"); - ctx.emitter.instruction("sub rsp, 64"); // reserve peer-name and client-cert/key spill storage - ctx.emitter.instruction("lea rdi, [rsp + 0]"); // pass peer-name out_ptr address - ctx.emitter.instruction("lea rsi, [rsp + 8]"); // pass peer-name out_len address - abi::emit_call_label(ctx.emitter, "__rt_get_ssl_peer_name"); - ctx.emitter.instruction("test rax, rax"); // did the context provide ssl.peer_name? - ctx.emitter.instruction(&format!("jnz {}", peer_ok)); // use ssl.peer_name when present - ctx.emitter.instruction("mov r10, QWORD PTR [rsp + 64]"); // reload fd for the connect-host table lookup - abi::emit_symbol_address(ctx.emitter, "r9", "_stream_connect_host"); - ctx.emitter.instruction("shl r10, 4"); // fd * 16, the host table stride - ctx.emitter.instruction("add r9, r10"); // address this fd's saved host pointer/length pair - ctx.emitter.instruction("mov r11, QWORD PTR [r9 + 8]"); // load the saved connection-host byte length - ctx.emitter.instruction("test r11, r11"); // is a connection host known for this fd? - ctx.emitter.instruction(&format!("jz {}", host_default)); // fall back to localhost when no host is known - ctx.emitter.instruction("mov r10, QWORD PTR [r9 + 0]"); // load the saved connection-host pointer - ctx.emitter.instruction("mov QWORD PTR [rsp + 0], r10"); // use the connection host as peer_name pointer - ctx.emitter.instruction("mov QWORD PTR [rsp + 8], r11"); // use the connection host as peer_name length - ctx.emitter.instruction(&format!("jmp {}", peer_ok)); // skip the localhost fallback - ctx.emitter.label(&host_default); - abi::emit_symbol_address(ctx.emitter, "r9", "_tls_peer_name_default"); - ctx.emitter.instruction("mov QWORD PTR [rsp + 0], r9"); // use localhost as the fallback peer_name pointer - ctx.emitter.instruction("mov r9, 9"); // strlen("localhost") - ctx.emitter.instruction("mov QWORD PTR [rsp + 8], r9"); // use localhost as the fallback peer_name length - ctx.emitter.label(&peer_ok); - - ctx.emitter.instruction("mov QWORD PTR [rsp + 24], 0"); // default local_cert length to zero - ctx.emitter.instruction("mov QWORD PTR [rsp + 40], 0"); // default local_pk length to zero - abi::emit_symbol_address(ctx.emitter, "rdi", "_ssl_key_str"); - ctx.emitter.instruction("mov rsi, 3"); // strlen("ssl") - abi::emit_symbol_address(ctx.emitter, "rdx", "_ssl_local_cert_key_str"); - ctx.emitter.instruction("mov rcx, 10"); // strlen("local_cert") - ctx.emitter.instruction("lea r8, [rsp + 16]"); // pass local_cert out_ptr address - ctx.emitter.instruction("lea r9, [rsp + 24]"); // pass local_cert out_len address - abi::emit_call_label(ctx.emitter, "__rt_get_string_context_option"); - abi::emit_symbol_address(ctx.emitter, "rdi", "_ssl_key_str"); - ctx.emitter.instruction("mov rsi, 3"); // strlen("ssl") - abi::emit_symbol_address(ctx.emitter, "rdx", "_ssl_local_pk_key_str"); - ctx.emitter.instruction("mov rcx, 8"); // strlen("local_pk") - ctx.emitter.instruction("lea r8, [rsp + 32]"); // pass local_pk out_ptr address - ctx.emitter.instruction("lea r9, [rsp + 40]"); // pass local_pk out_len address - abi::emit_call_label(ctx.emitter, "__rt_get_string_context_option"); - - ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 64]"); // reload fd as the first TLS attach argument - ctx.emitter.instruction("mov rsi, QWORD PTR [rsp + 0]"); // pass peer_name pointer - ctx.emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // pass peer_name byte length - ctx.emitter.instruction("mov rax, QWORD PTR [rsp + 24]"); // load local_cert byte length - ctx.emitter.instruction("test rax, rax"); // is a client certificate path present? - ctx.emitter.instruction(&format!("jz {}", plain_attach)); // no client certificate selects plain TLS attach - ctx.emitter.instruction("mov rax, QWORD PTR [rsp + 40]"); // load local_pk byte length - ctx.emitter.instruction("test rax, rax"); // is a client private key path present? - ctx.emitter.instruction(&format!("jz {}", plain_attach)); // missing key selects plain TLS attach - ctx.emitter.instruction("mov rcx, QWORD PTR [rsp + 16]"); // pass local_cert path pointer - ctx.emitter.instruction("mov r8, QWORD PTR [rsp + 24]"); // pass local_cert path length - ctx.emitter.instruction("mov rax, QWORD PTR [rsp + 40]"); // stage local_pk path length for the stack argument - ctx.emitter.instruction("mov r9, QWORD PTR [rsp + 32]"); // pass local_pk path pointer - ctx.emitter.instruction("sub rsp, 16"); // reserve the seventh stack argument plus padding - ctx.emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // pass local_pk path length as the seventh argument - abi::emit_load_symbol_to_reg(ctx.emitter, "r10", "_elephc_tls_attach_fd_client_cert_fn", 0); // load the mutual-TLS attach function pointer - ctx.emitter.instruction("call r10"); // attach TLS with a client certificate - ctx.emitter.instruction("add rsp, 16"); // release the seventh stack argument - ctx.emitter.instruction(&format!("jmp {}", after_attach)); // skip the default attach variant - ctx.emitter.label(&plain_attach); - ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 64]"); // reload fd as the first TLS attach argument - ctx.emitter.instruction("mov rsi, QWORD PTR [rsp + 0]"); // pass peer_name pointer - ctx.emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // pass peer_name byte length - abi::emit_load_symbol_to_reg(ctx.emitter, "r9", "_elephc_tls_attach_fd_fn", 0); // load the default TLS attach function pointer - ctx.emitter.instruction("call r9"); // attach TLS and return a session handle - ctx.emitter.label(&after_attach); - ctx.emitter.instruction("mov r10, QWORD PTR [rsp + 64]"); // reload fd before releasing the spill storage - abi::emit_release_temporary_stack(ctx.emitter, 64); - abi::emit_release_temporary_stack(ctx.emitter, 16); + let existing_session = ctx.next_label("ssec_existing_session_x"); + let handshake = ctx.next_label("ssec_handshake_x"); + let handshake_failed = ctx.next_label("ssec_handshake_failed_x"); + let release_success = ctx.next_label("ssec_release_success_x"); + let release_failure = ctx.next_label("ssec_release_failure_x"); + ctx.emitter.instruction("sub rsp, 112"); // reserve TLS options, session, status, and boolean scratch storage + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 112]"); // look up any TLS session already associated with this descriptor + abi::emit_call_label(ctx.emitter, "__rt_tls_session_get"); + ctx.emitter.instruction("test rax, rax"); // does this descriptor already own a rustls session? + ctx.emitter.instruction(&format!("jnz {}", existing_session)); // resume an in-progress handshake instead of attaching twice + emit_stream_crypto_tls_options_x86_64(ctx); + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 112]"); // pass the descriptor to the options-aware TLS attach bridge + ctx.emitter.instruction("mov rsi, rsp"); // pass the complete TLS client options structure + ctx.emitter.instruction("mov rdx, QWORD PTR [rsp + 120]"); // pass the requested PHP crypto method + ctx.emitter.instruction("mov rcx, QWORD PTR [rsp + 128]"); // distinguish explicit zero from an absent method + ctx.emitter.instruction("mov r8, QWORD PTR [rsp + 136]"); // pass an optional source TLS session for resumption + abi::emit_load_symbol_to_reg(ctx.emitter, "r9", "_elephc_tls_attach_fd_with_options_fn", 0); // load the options-aware TLS attach pointer + ctx.emitter.emit_published_bridge_call("r9"); // attach TLS with independent PHP verification policy ctx.emitter.instruction("cmp rax, 0"); // negative handles indicate TLS attach failure - ctx.emitter.instruction(&format!("jl {}", fail_label)); // return false when attach failed - abi::emit_symbol_address(ctx.emitter, "r11", "_tls_sessions"); - ctx.emitter.instruction("mov QWORD PTR [r11 + r10 * 8], rax"); // store the TLS session handle for this fd - ctx.emitter.instruction("mov eax, 1"); // return true after successful TLS attach + ctx.emitter.instruction(&format!("jl {}", fail_label)); // report attach failure after releasing storage + ctx.emitter.instruction("mov QWORD PTR [rsp + 88], rax"); // preserve the new session across table insertion + ctx.emitter.instruction("mov rsi, rax"); // pass the new session as the bounded-map value + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 112]"); // pass the full-width descriptor as the bounded-map key + abi::emit_call_label(ctx.emitter, "__rt_tls_session_set"); + ctx.emitter.instruction("test rax, rax"); // did the bounded table retain the new session? + ctx.emitter.instruction(&format!("jz {}", fail_label)); // table exhaustion closes the session and reports false + ctx.emitter.instruction(&format!("jmp {}", handshake)); // progress the newly attached TLS session + ctx.emitter.label(&existing_session); + ctx.emitter.instruction("mov QWORD PTR [rsp + 88], rax"); // preserve the existing TLS session across handshake + ctx.emitter.label(&handshake); + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 88]"); // pass the persisted TLS session handle to rustls + abi::emit_load_symbol_to_reg(ctx.emitter, "r9", "_elephc_tls_handshake_fn", 0); + ctx.emitter.emit_published_bridge_call("r9"); // complete or advance the TLS handshake through rustls + ctx.emitter.instruction("movsxd rax, eax"); // the bridge returns i32: the upper half of rax is undefined, so a + // negative status would read as a large positive 64-bit value + ctx.emitter.instruction("mov QWORD PTR [rsp + 96], rax"); // retain the PHP tri-state result across cleanup + ctx.emitter.instruction("cmp rax, 0"); // terminal handshake failure is negative + ctx.emitter.instruction(&format!("jl {}", handshake_failed)); // clear and close the failed TLS session + ctx.emitter.instruction(&format!("jmp {}", release_success)); // retain success or nonblocking progress + ctx.emitter.label(&release_success); + ctx.emitter.instruction("mov rax, QWORD PTR [rsp + 96]"); // reload the retained handshake status + abi::emit_release_temporary_stack(ctx.emitter, 112); + abi::emit_release_temporary_stack(ctx.emitter, 32); ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the failure result + ctx.emitter.label(&handshake_failed); + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 112]"); // clear the descriptor-to-session association + abi::emit_call_label(ctx.emitter, "__rt_tls_session_clear"); + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 88]"); // close the failed rustls session + abi::emit_load_symbol_to_reg(ctx.emitter, "r9", "_elephc_tls_close_fn", 0); + ctx.emitter.emit_published_bridge_call("r9"); // release the failed TLS session and duplicated socket + ctx.emitter.instruction("mov rax, -1"); // normalize terminal TLS errors to PHP false + ctx.emitter.instruction(&format!("jmp {}", release_failure)); // release temporary storage before returning ctx.emitter.label(&fail_label); - ctx.emitter.instruction("xor eax, eax"); // return false after TLS attach failure + ctx.emitter.instruction("mov rax, -1"); // return false after TLS attach or table failure + ctx.emitter.label(&release_failure); + abi::emit_release_temporary_stack(ctx.emitter, 112); + abi::emit_release_temporary_stack(ctx.emitter, 32); + ctx.emitter.instruction(&format!("jmp {}", done_label)); // normalize failures through PHP return boxing +} + +/// Boxes the raw TLS bridge status into PHP's `true` / `0` / `false` result. +fn box_stream_socket_enable_crypto_status(ctx: &mut FunctionContext<'_>) { + let complete = ctx.next_label("ssec_status_complete"); + let progress = ctx.next_label("ssec_status_progress"); + let done = ctx.next_label("ssec_status_done"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("cmp x0, #1"); // only a completed rustls handshake maps to PHP true + ctx.emitter.instruction(&format!("b.eq {}", complete)); // branch before treating zero as nonblocking progress + ctx.emitter.instruction(&format!("cbz x0, {}", progress)); // PHP uses integer zero while the handshake is still pending + ctx.emitter.instruction("mov x0, #0"); // terminal bridge errors map to PHP false + emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Bool); + ctx.emitter.instruction(&format!("b {}", done)); // avoid boxing the false result again + ctx.emitter.label(&progress); + ctx.emitter.instruction("mov x0, #0"); // preserve the integer-zero payload for PHP's retry signal + emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Int); + ctx.emitter.instruction(&format!("b {}", done)); // preserve the boxed integer result + ctx.emitter.label(&complete); + ctx.emitter.instruction("mov x0, #1"); // PHP true represents a fully authenticated TLS session + emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Bool); + ctx.emitter.label(&done); + } + Arch::X86_64 => { + ctx.emitter.instruction("cmp rax, 1"); // only a completed rustls handshake maps to PHP true + ctx.emitter.instruction(&format!("je {}", complete)); // branch before treating zero as nonblocking progress + ctx.emitter.instruction("test rax, rax"); // raw zero is the bridge's retryable status + ctx.emitter.instruction(&format!("jz {}", progress)); // PHP uses integer zero while the handshake is still pending + ctx.emitter.instruction("xor eax, eax"); // terminal bridge errors map to PHP false + emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Bool); + ctx.emitter.instruction(&format!("jmp {}", done)); // avoid boxing the false result again + ctx.emitter.label(&progress); + ctx.emitter.instruction("xor eax, eax"); // preserve the integer-zero payload for PHP's retry signal + emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Int); + ctx.emitter.instruction(&format!("jmp {}", done)); // preserve the boxed integer result + ctx.emitter.label(&complete); + ctx.emitter.instruction("mov eax, 1"); // PHP true represents a fully authenticated TLS session + emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Bool); + ctx.emitter.label(&done); + } + } } /// Reserves temporary storage for `stream_get_contents` fd and length operands. @@ -8304,6 +9411,11 @@ fn lower_fseek_x86_64( ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip EOF reset after a failed seek ctx.emitter.label(success_label); abi::emit_pop_reg(ctx.emitter, "r10"); + if ctx.emitter.target.platform == Platform::Windows { + ctx.emitter.instruction("mov rdi, r10"); // map the opaque CRT descriptor before indexing bounded stream state + abi::emit_call_label(ctx.emitter, "__rt_win_stream_slot"); + ctx.emitter.instruction("mov r10, rax"); // use the compact Windows stream-state slot for EOF reset + } ctx.emitter.instruction("lea r11, [rip + _eof_flags]"); // materialize the EOF-flag table base ctx.emitter.instruction("mov BYTE PTR [r11 + r10], 0"); // clear EOF state for the successfully repositioned stream ctx.emitter.instruction("xor eax, eax"); // fseek returns 0 after a successful seek @@ -8376,6 +9488,11 @@ fn lower_rewind_x86_64( ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip EOF reset after a failed rewind ctx.emitter.label(success_label); abi::emit_pop_reg(ctx.emitter, "r10"); + if ctx.emitter.target.platform == Platform::Windows { + ctx.emitter.instruction("mov rdi, r10"); // map the opaque CRT descriptor before indexing bounded stream state + abi::emit_call_label(ctx.emitter, "__rt_win_stream_slot"); + ctx.emitter.instruction("mov r10, rax"); // use the compact Windows stream-state slot for EOF reset + } ctx.emitter.instruction("lea r11, [rip + _eof_flags]"); // materialize the EOF-flag table base ctx.emitter.instruction("mov BYTE PTR [r11 + r10], 0"); // clear EOF state after rewinding the stream ctx.emitter.instruction("mov rax, 1"); // rewind returns true after a successful seek diff --git a/src/codegen/lower_inst/builtins/math.rs b/src/codegen/lower_inst/builtins/math.rs index 14936f306b..4e3386443d 100644 --- a/src/codegen/lower_inst/builtins/math.rs +++ b/src/codegen/lower_inst/builtins/math.rs @@ -28,7 +28,7 @@ pub(crate) use binary::{lower_fdiv, lower_fmod, lower_intdiv, lower_pow}; pub(crate) use libm::{ lower_atan2, lower_deg2rad, lower_hypot, lower_log, lower_rad2deg, lower_unary_libm, }; -pub(crate) use random::{lower_rand, lower_random_int}; +pub(crate) use random::{lower_rand, lower_random_bytes, lower_random_int}; 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"; @@ -259,7 +259,7 @@ pub(crate) fn lower_pi(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Res match ctx.emitter.target.arch { Arch::AArch64 => { ctx.emitter.adrp("x9", &label); // load the page address that contains the M_PI floating constant - ctx.emitter.ldr_lo12("d0", "x9", &label); // load the M_PI floating constant into the floating result register + ctx.emitter.ldr_lo12("d0", "x9", &label); // load the M_PI floating constant into the floating result register } Arch::X86_64 => { ctx.emitter.instruction(&format!("movsd xmm0, QWORD PTR [rip + {}]", label)); // load the M_PI floating constant into the floating result register @@ -721,57 +721,54 @@ fn lower_float_rounding_builtin( store_if_result(ctx, inst) } -/// Rounds the loaded float to the nearest integer using PHP's ties-away behavior. +/// Rounds the loaded float to PHP's `round($x)` (implicit `precision = 0`) by +/// delegating to the shared `__rt_php_round` port of php-src's +/// `_php_math_round` (`PHP_ROUND_HALF_UP` mode) — see +/// `crate::codegen_support::runtime::strings::php_round` for why the naive +/// hardware ties-away rounding this used to emit (`frinta`/libc `round()`) +/// diverges from PHP on inputs like `1.005` whose binary representation +/// already carries error before any rounding happens. fn emit_round_loaded_float(ctx: &mut FunctionContext<'_>) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("frinta d0, d0"); // round to nearest with ties away from zero + ctx.emitter.instruction("mov x1, #0"); // places = 0 (round($x) has no precision argument) } Arch::X86_64 => { - ctx.emitter.bl_c("round"); + ctx.emitter.instruction("xor edi, edi"); // places = 0 (round($x) has no precision argument) } } + abi::emit_call_label(ctx.emitter, "__rt_php_round"); } -/// Rounds the loaded float after applying the optional decimal precision. +/// Rounds the loaded float at the requested decimal precision via the shared +/// `__rt_php_round` port of php-src's `_php_math_round` (`PHP_ROUND_HALF_UP` +/// mode), replacing the naive "multiply by 10**places, round, divide back" +/// scaling this used to emit — that scaling has no defense against the +/// binary-representation error the `10**places` multiplication introduces +/// (e.g. `round(1.005, 2)` scaled naively rounds `100.49999999999999` to +/// `100`, giving `1.0` instead of PHP's `1.01`); `__rt_php_round` carries +/// php-src's pre-correction step that detects and fixes exactly this case. fn emit_round_loaded_float_with_precision( ctx: &mut FunctionContext<'_>, inst: &Instruction, ) -> Result<()> { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("str d0, [sp, #-16]!"); // preserve the round() value while computing the precision multiplier + ctx.emitter.instruction("str d0, [sp, #-16]!"); // preserve the value while evaluating the precision operand let precision = expect_operand(inst, 1)?; load_precision_as_int(ctx, precision, "round")?; - ctx.emitter.instruction("scvtf d1, x0"); // convert the precision to a floating exponent for pow() - ctx.emitter.instruction("str d1, [sp, #-16]!"); // preserve the exponent while materializing the pow() base - ctx.emitter.instruction("fmov d0, #10.0"); // materialize 10.0 as the precision multiplier base - ctx.emitter.instruction("ldr d1, [sp], #16"); // restore the exponent into the second pow() argument - ctx.emitter.bl_c("pow"); - ctx.emitter.instruction("ldr d1, [sp], #16"); // restore the original value after pow() returns the multiplier - ctx.emitter.instruction("fmul d1, d1, d0"); // scale the original value by the precision multiplier - ctx.emitter.instruction("str d0, [sp, #-16]!"); // preserve the multiplier for the final division - ctx.emitter.instruction("frinta d0, d1"); // round the scaled value with ties away from zero - ctx.emitter.instruction("ldr d1, [sp], #16"); // restore the precision multiplier for rescaling - ctx.emitter.instruction("fdiv d0, d0, d1"); // scale the rounded value back to the requested precision + ctx.emitter.instruction("mov x1, x0"); // places = the requested precision + ctx.emitter.instruction("ldr d0, [sp], #16"); // restore the original value } Arch::X86_64 => { abi::emit_push_float_reg(ctx.emitter, "xmm0"); let precision = expect_operand(inst, 1)?; load_precision_as_int(ctx, precision, "round")?; - ctx.emitter.instruction("cvtsi2sd xmm1, rax"); // convert the precision to a floating exponent for pow() - ctx.emitter.instruction("mov rax, 0x4024000000000000"); // materialize the IEEE-754 payload for 10.0 - ctx.emitter.instruction("movq xmm0, rax"); // move 10.0 into the first pow() argument - ctx.emitter.bl_c("pow"); - abi::emit_pop_float_reg(ctx.emitter, "xmm1"); - ctx.emitter.instruction("mulsd xmm1, xmm0"); // scale the original value by the precision multiplier - abi::emit_push_float_reg(ctx.emitter, "xmm0"); - ctx.emitter.instruction("movsd xmm0, xmm1"); // move the scaled value into the round() argument register - ctx.emitter.bl_c("round"); - abi::emit_pop_float_reg(ctx.emitter, "xmm1"); - ctx.emitter.instruction("divsd xmm0, xmm1"); // scale the rounded value back to the requested precision + ctx.emitter.instruction("mov rdi, rax"); // places = the requested precision + abi::emit_pop_float_reg(ctx.emitter, "xmm0"); } } + abi::emit_call_label(ctx.emitter, "__rt_php_round"); Ok(()) } diff --git a/src/codegen/lower_inst/builtins/math/binary.rs b/src/codegen/lower_inst/builtins/math/binary.rs index 861b043ce2..f70811044c 100644 --- a/src/codegen/lower_inst/builtins/math/binary.rs +++ b/src/codegen/lower_inst/builtins/math/binary.rs @@ -110,7 +110,7 @@ pub(crate) fn lower_fmod( ctx.emitter.instruction("movapd xmm2, xmm0"); // preserve the divisor while ordering libc fmod arguments ctx.emitter.instruction("movapd xmm0, xmm1"); // move the dividend into the first libc fmod argument ctx.emitter.instruction("movapd xmm1, xmm2"); // move the divisor into the second libc fmod argument - ctx.emitter.bl_c("fmod"); + ctx.emitter.emit_call_c("fmod"); // Windows-safe: shadow space via __rt_sys_fmod } } store_if_result(ctx, inst) @@ -131,14 +131,14 @@ pub(crate) fn lower_pow( Arch::AArch64 => { ctx.emitter.instruction("fmov d1, d0"); // move the exponent into the second libc pow argument abi::emit_pop_float_reg(ctx.emitter, "d0"); - ctx.emitter.bl_c("pow"); + ctx.emitter.emit_call_c("pow"); } Arch::X86_64 => { abi::emit_pop_float_reg(ctx.emitter, "xmm1"); ctx.emitter.instruction("movapd xmm2, xmm0"); // preserve the exponent while ordering libc pow arguments ctx.emitter.instruction("movapd xmm0, xmm1"); // move the base into the first libc pow argument ctx.emitter.instruction("movapd xmm1, xmm2"); // move the exponent into the second libc pow argument - ctx.emitter.bl_c("pow"); + ctx.emitter.emit_call_c("pow"); // Windows-safe: shadow space via __rt_sys_pow } } store_if_result(ctx, inst) diff --git a/src/codegen/lower_inst/builtins/math/libm.rs b/src/codegen/lower_inst/builtins/math/libm.rs index d180dbddf0..5a29a6ac3c 100644 --- a/src/codegen/lower_inst/builtins/math/libm.rs +++ b/src/codegen/lower_inst/builtins/math/libm.rs @@ -27,7 +27,7 @@ pub(crate) fn lower_unary_libm( super::ensure_arg_count(inst, name, 1)?; let value = expect_operand(inst, 0)?; super::load_numeric_as_float(ctx, value, name)?; - ctx.emitter.bl_c(name); + ctx.emitter.emit_call_c(name); // Windows-safe: routes through the registered __rt_sys_ FP shim (sin/cos/tan/asin/acos/atan/sinh/cosh/tanh/exp/log2/log10) store_if_result(ctx, inst) } @@ -60,12 +60,19 @@ pub(crate) fn lower_log( } let value = expect_operand(inst, 0)?; super::load_numeric_as_float(ctx, value, "log")?; - ctx.emitter.bl_c("log"); + ctx.emitter.emit_call_c("log"); // Windows-safe: shadow space via __rt_sys_log if inst.operands.len() == 2 { + // Preserve log(value) across the second call. `emit_push_float_reg` on + // x86_64 reserves a full 16-byte slot (`sub rsp, 16`), so it never + // changes the parity of rsp mod 16 — the stack alignment seen by this + // second `emit_call_c("log")` is identical to the first, and the + // shim's own `sub rsp, 40` shadow-space reservation re-aligns it to a + // 16-byte boundary exactly as it did for the first call. No extra + // adjustment is needed around the change-of-base path. abi::emit_push_float_reg(ctx.emitter, abi::float_result_reg(ctx.emitter)); let base = expect_operand(inst, 1)?; super::load_numeric_as_float(ctx, base, "log")?; - ctx.emitter.bl_c("log"); + ctx.emitter.emit_call_c("log"); // Windows-safe: shadow space via __rt_sys_log (2nd call, change-of-base — see alignment note above) emit_change_of_base_divide(ctx); } store_if_result(ctx, inst) @@ -110,7 +117,7 @@ fn lower_binary_libm( ctx.emitter.instruction("movapd xmm2, xmm0"); // preserve the second operand while ordering SysV libm arguments ctx.emitter.instruction("movapd xmm0, xmm1"); // move the first operand into the first libm argument register ctx.emitter.instruction("movapd xmm1, xmm2"); // move the second operand into the second libm argument register - ctx.emitter.bl_c(name); + ctx.emitter.emit_call_c(name); // Windows-safe: routes through the registered __rt_sys_ FP shim (atan2/hypot) } } store_if_result(ctx, inst) diff --git a/src/codegen/lower_inst/builtins/math/random.rs b/src/codegen/lower_inst/builtins/math/random.rs index 4f21d8b516..af3d907c99 100644 --- a/src/codegen/lower_inst/builtins/math/random.rs +++ b/src/codegen/lower_inst/builtins/math/random.rs @@ -15,6 +15,7 @@ use crate::ir::{Instruction, ValueId}; use crate::types::PhpType; use super::super::super::super::context::FunctionContext; +use super::super::super::load_value_to_first_int_arg; use super::super::{expect_operand, store_if_result}; /// Lowers `rand()` and `mt_rand()` with either zero args or an inclusive range. @@ -46,6 +47,34 @@ pub(crate) fn lower_random_int( store_if_result(ctx, inst) } +/// 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. +pub(crate) fn lower_random_bytes( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + super::ensure_arg_count(inst, "random_bytes", 1)?; + let length = expect_operand(inst, 0)?; + load_numeric_as_int(ctx, length, "random_bytes")?; + match ctx.emitter.target.arch { + Arch::AArch64 => { + // length already sits in the AArch64 integer result register (x0), + // which is exactly where __rt_random_bytes expects it. + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rdi, rax"); // pass the requested byte length as the SysV first argument + } + } + abi::emit_call_label(ctx.emitter, "__rt_random_bytes"); + store_if_result(ctx, inst) +} + /// Emits the shared inclusive-range lowering for random integer builtins. fn lower_random_range( ctx: &mut FunctionContext<'_>, @@ -64,24 +93,39 @@ fn lower_random_range( } /// Emits the AArch64 range normalization and runtime call. +/// +/// Passes the *inclusive* width `max - min` to `__rt_random_uniform64`. Adding one +/// here to form an exclusive bound would wrap to zero for a full-width range such +/// as `random_int(PHP_INT_MIN, PHP_INT_MAX)`, and would additionally be truncated +/// to 32 bits by the older `__rt_random_uniform` entry point. fn emit_aarch64_random_range(ctx: &mut FunctionContext<'_>) -> Result<()> { abi::emit_pop_reg(ctx.emitter, "x9"); ctx.emitter.instruction("sub x0, x0, x9"); // compute the inclusive range width as max - min - ctx.emitter.instruction("add x0, x0, #1"); // convert the width to the exclusive upper bound for the random helper abi::emit_push_reg(ctx.emitter, "x9"); - abi::emit_call_label(ctx.emitter, "__rt_random_uniform"); + abi::emit_call_label(ctx.emitter, "__rt_random_uniform64"); abi::emit_pop_reg(ctx.emitter, "x9"); ctx.emitter.instruction("add x0, x0, x9"); // shift the sampled offset back into the caller-visible range Ok(()) } /// Emits the x86_64 range normalization and runtime call. +/// +/// Passes the *inclusive* width `max - min` to `__rt_random_uniform64`; see +/// `emit_aarch64_random_range` for why the exclusive form cannot be used. +/// `min` is spilled across the call, as the AArch64 arm already does. `r9` is +/// caller-saved in both SysV and MSx64, so holding it there only ever worked by +/// accident of which registers the sampler happened to touch. On Linux the entropy +/// draw is a bare `syscall`, which clobbers just `rcx` and `r11`; on Windows the +/// same site is rewritten into a call reaching `BCryptGenRandom`, and a Win32 call +/// clobbers `r9`. `random_int(-100, -50)` then returned values in `[0, 50]` -- +/// `min` dropped entirely -- while every fixture with `min == 0` stayed green. fn emit_x86_64_random_range(ctx: &mut FunctionContext<'_>) -> Result<()> { abi::emit_pop_reg(ctx.emitter, "r9"); ctx.emitter.instruction("sub rax, r9"); // compute the inclusive range width as max - min - ctx.emitter.instruction("add rax, 1"); // convert the width to the exclusive upper bound for the random helper - ctx.emitter.instruction("mov rdi, rax"); // pass the exclusive upper bound to the random helper - abi::emit_call_label(ctx.emitter, "__rt_random_uniform"); + ctx.emitter.instruction("mov rdi, rax"); // pass the inclusive range width to the random helper + abi::emit_push_reg(ctx.emitter, "r9"); + abi::emit_call_label(ctx.emitter, "__rt_random_uniform64"); + abi::emit_pop_reg(ctx.emitter, "r9"); ctx.emitter.instruction("add rax, r9"); // shift the sampled offset back into the caller-visible range Ok(()) } @@ -94,6 +138,10 @@ fn load_numeric_as_int( ) -> Result<()> { match ctx.load_value_to_result(value)?.codegen_repr() { PhpType::Int | PhpType::Bool => Ok(()), + PhpType::TaggedScalar => { + crate::codegen::sentinels::emit_tagged_scalar_to_int_null_as_zero(ctx.emitter); + Ok(()) + } PhpType::Void | PhpType::Never => { abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); Ok(()) @@ -102,6 +150,11 @@ fn load_numeric_as_int( abi::emit_float_result_to_int_result(ctx.emitter); Ok(()) } + PhpType::Mixed | PhpType::Union(_) => { + load_value_to_first_int_arg(ctx, value)?; + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_int"); + Ok(()) + } other => Err(CodegenIrError::unsupported(format!( "{} for PHP type {:?}", name, other diff --git a/src/codegen/lower_inst/builtins/spl.rs b/src/codegen/lower_inst/builtins/spl.rs index 61e335ddde..d0d16ab8c8 100644 --- a/src/codegen/lower_inst/builtins/spl.rs +++ b/src/codegen/lower_inst/builtins/spl.rs @@ -944,8 +944,8 @@ fn emit_loaded_hash_as_mixed(ctx: &mut FunctionContext<'_>) { /// Allocates an indexed array whose slots store boxed Mixed values. fn emit_new_mixed_indexed_array(ctx: &mut FunctionContext<'_>) { - abi::emit_load_int_immediate(ctx.emitter, abi::int_arg_reg_name(ctx.emitter.target, 0), 16); - abi::emit_load_int_immediate(ctx.emitter, abi::int_arg_reg_name(ctx.emitter.target, 1), 8); + abi::emit_load_int_immediate(ctx.emitter, abi::runtime_helper_int_arg_reg(ctx.emitter, 0), 16); + abi::emit_load_int_immediate(ctx.emitter, abi::runtime_helper_int_arg_reg(ctx.emitter, 1), 8); abi::emit_call_label(ctx.emitter, "__rt_array_new"); crate::codegen::emit_array_value_type_stamp( ctx.emitter, @@ -956,10 +956,10 @@ fn emit_new_mixed_indexed_array(ctx: &mut FunctionContext<'_>) { /// Allocates an associative array whose values are boxed Mixed cells. fn emit_new_mixed_hash(ctx: &mut FunctionContext<'_>) { - abi::emit_load_int_immediate(ctx.emitter, abi::int_arg_reg_name(ctx.emitter.target, 0), 16); + abi::emit_load_int_immediate(ctx.emitter, abi::runtime_helper_int_arg_reg(ctx.emitter, 0), 16); abi::emit_load_int_immediate( ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), + abi::runtime_helper_int_arg_reg(ctx.emitter, 1), runtime_value_tag(&PhpType::Mixed) as i64, ); abi::emit_call_label(ctx.emitter, "__rt_hash_new"); @@ -1571,10 +1571,16 @@ fn emit_branch_if_saved_receiver_implements( } /// Moves the loaded object result into the ABI receiver register for method dispatch. +/// Targets the platform receiver register (rdi SysV, rcx MSx64) so the resolved interface +/// method — a codegen-emitted callee — finds $this where it expects it. fn move_result_to_receiver_arg(ctx: &mut FunctionContext<'_>) { - if ctx.emitter.target.arch == Arch::X86_64 { - ctx.emitter.instruction("mov rdi, rax"); // pass the object result as the interface method receiver + let result_reg = abi::int_result_reg(ctx.emitter); + let arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); + if result_reg == arg_reg { + return; } + ctx.emitter + .instruction(&format!("mov {}, {}", arg_reg, result_reg)); // pass the object result as the interface method receiver } /// Reloads the saved iterator receiver from the top temporary stack slot. @@ -1584,7 +1590,9 @@ fn reload_saved_iterator_receiver(ctx: &mut FunctionContext<'_>) { ctx.emitter.instruction("ldr x0, [sp]"); // reload iterator receiver before the next protocol call } Arch::X86_64 => { - ctx.emitter.instruction("mov rdi, QWORD PTR [rsp]"); // reload iterator receiver before the next protocol call + let receiver_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); // platform receiver register (rdi SysV, rcx MSx64) + ctx.emitter + .instruction(&format!("mov {}, QWORD PTR [rsp]", receiver_reg)); // reload iterator receiver before the next protocol call } } } @@ -1599,7 +1607,11 @@ fn reload_saved_iterator_receiver_at_offset( ctx.emitter.instruction(&format!("ldr x0, [sp, #{}]", offset)); // reload iterator receiver from below preserved key state } Arch::X86_64 => { - ctx.emitter.instruction(&format!("mov rdi, QWORD PTR [rsp + {}]", offset)); // reload iterator receiver from below preserved key state + let receiver_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); // platform receiver register (rdi SysV, rcx MSx64) + ctx.emitter.instruction(&format!( // reload the iterator receiver beneath preserved key state + "mov {}, QWORD PTR [rsp + {}]", + receiver_reg, offset + )); } } } diff --git a/src/codegen/lower_inst/builtins/strings.rs b/src/codegen/lower_inst/builtins/strings.rs index d5e06262c6..cb905682ef 100644 --- a/src/codegen/lower_inst/builtins/strings.rs +++ b/src/codegen/lower_inst/builtins/strings.rs @@ -658,7 +658,8 @@ pub(super) fn sprintf_spec_cats_for_format( /// Parses the conversion categories consumed by the runtime sprintf scanner. fn parse_sprintf_spec_cats(format: &[u8]) -> Vec { - let mut cats = Vec::new(); + let mut cats: Vec> = Vec::new(); + let mut sequential = 0usize; let mut index = 0; while index < format.len() { if format[index] != b'%' { @@ -673,11 +674,47 @@ fn parse_sprintf_spec_cats(format: &[u8]) -> Vec { index += 1; continue; } - while index < format.len() - && matches!(format[index], b'-' | b'+' | b'0' | b' ' | b'#') - { + // php lets a specifier name its argument as "N$" ahead of the flags. The + // digits are ambiguous with a width, so they only count as a position when a + // '$' follows; otherwise the cursor rewinds and they are read as the width. + // Without this the '$' was taken for the conversion character, so the + // argument of "%1$s" was packed as an integer and reached the runtime with a + // garbage length. + let mut position = None; + let digits_start = index; + while index < format.len() && format[index].is_ascii_digit() { index += 1; } + if index > digits_start && index < format.len() && format[index] == b'$' { + let digits = &format[digits_start..index]; + let value = digits + .iter() + .fold(0usize, |acc, byte| acc.saturating_mul(10) + usize::from(byte - b'0')); + if value >= 1 { + position = Some(value - 1); + } + index += 1; + } else { + index = digits_start; + } + // Flags and the quoted pad character interleave freely: php accepts both + // "%'x-8s" and "%-'x8s". A quoted pad consumes exactly the byte that follows + // it, which may be a flag, a digit or even the conversion letter -- reading + // them in a fixed order made "%'x-8s" push a category for '-' and pack its + // string argument as an integer. + loop { + if index < format.len() + && matches!(format[index], b'-' | b'+' | b'0' | b' ' | b'#') + { + index += 1; + continue; + } + if index + 1 < format.len() && format[index] == b'\'' { + index += 2; + continue; + } + break; + } while index < format.len() && format[index].is_ascii_digit() { index += 1; } @@ -690,14 +727,31 @@ fn parse_sprintf_spec_cats(format: &[u8]) -> Vec { if index >= format.len() { break; } - cats.push(match format[index] { - b'f' | b'e' | b'g' => SprintfSpecCat::Float, + let cat = match format[index] { + b'f' | b'e' | b'g' | b'E' | b'G' => SprintfSpecCat::Float, b's' => SprintfSpecCat::Str, _ => SprintfSpecCat::Int, - }); + }; + // A positional specifier does not advance the sequential counter, matching + // the runtime scanner: "%s|%2$s|%s" reads arguments 0, 1 and 1. + let slot = match position { + Some(slot) => slot, + None => { + let slot = sequential; + sequential += 1; + slot + } + }; + if slot >= cats.len() { + cats.resize(slot + 1, None); + } + cats[slot] = Some(cat); index += 1; } cats + .into_iter() + .map(|cat| cat.unwrap_or(SprintfSpecCat::Int)) + .collect() } /// Preserves the format string, evaluates the values array, and calls `__rt_vsprintf`. @@ -1076,7 +1130,7 @@ fn lower_gzcompress_x86_64(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> ctx.emitter.instruction("mov QWORD PTR [rsp + 8], rsi"); // save the source pointer ctx.emitter.instruction("mov QWORD PTR [rsp + 16], rdx"); // save the source length ctx.emitter.instruction("mov rdi, rdx"); // pass the source length to compressBound - ctx.emitter.instruction("call compressBound"); // compute the worst-case compressed byte length + ctx.emitter.emit_call_c("compressBound"); // compute the worst-case compressed byte length ctx.emitter.instruction("mov QWORD PTR [rsp + 24], rax"); // seed destLen with the output capacity ctx.emitter.instruction("call __rt_heap_alloc"); // allocate the compressed-data buffer ctx.emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(1))); // materialize the x86_64 string heap kind word @@ -1087,7 +1141,7 @@ fn lower_gzcompress_x86_64(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> ctx.emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // pass the source pointer ctx.emitter.instruction("mov rcx, QWORD PTR [rsp + 16]"); // pass the source length ctx.emitter.instruction("mov r8, QWORD PTR [rsp + 0]"); // pass the requested compression level - ctx.emitter.instruction("call compress2"); // zlib-compress the source into the output buffer + ctx.emitter.emit_call_c("compress2"); // zlib-compress the source into the output buffer ctx.emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // return the compressed string pointer ctx.emitter.instruction("mov rdx, QWORD PTR [rsp + 24]"); // return the compressed string length ctx.emitter.instruction("add rsp, 64"); // release the zlib scratch storage @@ -1160,13 +1214,14 @@ fn lower_gzdeflate_x86_64( zero: &str, zeroed: &str, ) -> Result<()> { + let zstream = crate::codegen_support::stream_filters::zlib::z_stream_layout(ctx.emitter.target); materialize_gz_level_x86_64(ctx, inst, "gzdeflate level")?; ctx.emitter.instruction("sub rsp, 160"); // reserve z_stream storage plus scratch slots ctx.emitter.instruction("mov QWORD PTR [rsp + 136], rdi"); // save the compression level ctx.emitter.instruction("mov QWORD PTR [rsp + 112], rsi"); // save the source pointer ctx.emitter.instruction("mov QWORD PTR [rsp + 120], rdx"); // save the source length ctx.emitter.instruction("mov rdi, rdx"); // pass the source length to compressBound - ctx.emitter.instruction("call compressBound"); // compute the worst-case compressed byte length + ctx.emitter.emit_call_c("compressBound"); // compute the worst-case compressed byte length ctx.emitter.instruction("mov QWORD PTR [rsp + 144], rax"); // save the output capacity ctx.emitter.instruction("call __rt_heap_alloc"); // allocate the compressed-data buffer ctx.emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(1))); // materialize the x86_64 string heap kind word @@ -1175,7 +1230,7 @@ fn lower_gzdeflate_x86_64( ctx.emitter.instruction("xor r9, r9"); // initialize the z_stream clear index ctx.emitter.label(zero); - ctx.emitter.instruction("cmp r9, 112"); // check whether every z_stream byte is cleared + ctx.emitter.instruction(&format!("cmp r9, {}", zstream.size)); // check whether every target z_stream byte is cleared ctx.emitter.instruction(&format!("jge {}", zeroed)); // continue after the z_stream has been zeroed ctx.emitter.instruction("mov BYTE PTR [rsp + r9], 0"); // clear one z_stream byte ctx.emitter.instruction("inc r9"); // advance the z_stream clear index @@ -1191,24 +1246,28 @@ fn lower_gzdeflate_x86_64( ctx.emitter.instruction("sub rsp, 16"); // reserve stack slots for the last deflateInit2_ args abi::emit_symbol_address(ctx.emitter, "rax", "_zlib_version"); ctx.emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // pass the zlib version string on the stack - ctx.emitter.instruction("mov QWORD PTR [rsp + 8], 112"); // pass sizeof(z_stream) on the stack - ctx.emitter.instruction("call deflateInit2_"); // initialize the raw-deflate zlib stream + ctx.emitter.instruction(&format!("mov QWORD PTR [rsp + 8], {}", zstream.size)); // pass target sizeof(z_stream) on the stack + ctx.emitter.emit_call_c("deflateInit2_"); // initialize the raw-deflate zlib stream ctx.emitter.instruction("add rsp, 16"); // release deflateInit2_ stack arguments ctx.emitter.instruction("mov r9, QWORD PTR [rsp + 112]"); // reload the source pointer ctx.emitter.instruction("mov QWORD PTR [rsp + 0], r9"); // set z_stream.next_in ctx.emitter.instruction("mov r9, QWORD PTR [rsp + 120]"); // reload the source length ctx.emitter.instruction("mov DWORD PTR [rsp + 8], r9d"); // set z_stream.avail_in ctx.emitter.instruction("mov r9, QWORD PTR [rsp + 128]"); // reload the destination pointer - ctx.emitter.instruction("mov QWORD PTR [rsp + 24], r9"); // set z_stream.next_out + ctx.emitter.instruction(&format!("mov QWORD PTR [rsp + {}], r9", zstream.next_out)); // set z_stream.next_out ctx.emitter.instruction("mov r9, QWORD PTR [rsp + 144]"); // reload the destination capacity - ctx.emitter.instruction("mov DWORD PTR [rsp + 32], r9d"); // set z_stream.avail_out + ctx.emitter.instruction(&format!("mov DWORD PTR [rsp + {}], r9d", zstream.avail_out)); // set z_stream.avail_out ctx.emitter.instruction("mov rdi, rsp"); // pass the z_stream pointer to deflate ctx.emitter.instruction("mov esi, 4"); // request a final deflate pass - ctx.emitter.instruction("call deflate"); // compress the full input - ctx.emitter.instruction("mov rax, QWORD PTR [rsp + 40]"); // read z_stream.total_out + ctx.emitter.emit_call_c("deflate"); // compress the full input + if ctx.emitter.target.platform == crate::codegen_support::platform::Platform::Windows { + ctx.emitter.instruction(&format!("mov eax, DWORD PTR [rsp + {}]", zstream.total_out)); // LLP64 z_stream.total_out is a u32 + } else { + ctx.emitter.instruction(&format!("mov rax, QWORD PTR [rsp + {}]", zstream.total_out)); // LP64 z_stream.total_out is a u64 + } ctx.emitter.instruction("mov QWORD PTR [rsp + 152], rax"); // save the compressed length across deflateEnd ctx.emitter.instruction("mov rdi, rsp"); // pass the z_stream pointer to deflateEnd - ctx.emitter.instruction("call deflateEnd"); // release zlib's internal deflate state + ctx.emitter.emit_call_c("deflateEnd"); // release zlib's internal deflate state ctx.emitter.instruction("mov rax, QWORD PTR [rsp + 128]"); // return the compressed string pointer ctx.emitter.instruction("mov rdx, QWORD PTR [rsp + 152]"); // return the compressed string length ctx.emitter.instruction("add rsp, 160"); // release z_stream scratch storage @@ -1288,6 +1347,7 @@ fn lower_gzinflate_x86_64( fail: &str, done: &str, ) { + let zstream = crate::codegen_support::stream_filters::zlib::z_stream_layout(ctx.emitter.target); let sized = format!("{}_sized", zero); ctx.emitter.instruction("sub rsp, 160"); // reserve z_stream storage plus scratch slots ctx.emitter.instruction("mov QWORD PTR [rsp + 112], rax"); // save the source pointer @@ -1307,7 +1367,7 @@ fn lower_gzinflate_x86_64( ctx.emitter.instruction("xor r9, r9"); // initialize the z_stream clear index ctx.emitter.label(zero); - ctx.emitter.instruction("cmp r9, 112"); // check whether every z_stream byte is cleared + ctx.emitter.instruction(&format!("cmp r9, {}", zstream.size)); // check whether every target z_stream byte is cleared ctx.emitter.instruction(&format!("jge {}", zeroed)); // continue after the z_stream has been zeroed ctx.emitter.instruction("mov BYTE PTR [rsp + r9], 0"); // clear one z_stream byte ctx.emitter.instruction("inc r9"); // advance the z_stream clear index @@ -1317,24 +1377,28 @@ fn lower_gzinflate_x86_64( ctx.emitter.instruction("mov rdi, rsp"); // pass the z_stream pointer ctx.emitter.instruction("mov esi, -15"); // request raw inflate with negative window bits abi::emit_symbol_address(ctx.emitter, "rdx", "_zlib_version"); - ctx.emitter.instruction("mov ecx, 112"); // pass sizeof(z_stream) - ctx.emitter.instruction("call inflateInit2_"); // initialize the raw-inflate zlib stream + ctx.emitter.instruction(&format!("mov ecx, {}", zstream.size)); // pass target sizeof(z_stream) + ctx.emitter.emit_call_c("inflateInit2_"); // initialize the raw-inflate zlib stream ctx.emitter.instruction("mov r9, QWORD PTR [rsp + 112]"); // reload the source pointer ctx.emitter.instruction("mov QWORD PTR [rsp + 0], r9"); // set z_stream.next_in ctx.emitter.instruction("mov r9, QWORD PTR [rsp + 120]"); // reload the source length ctx.emitter.instruction("mov DWORD PTR [rsp + 8], r9d"); // set z_stream.avail_in ctx.emitter.instruction("mov r9, QWORD PTR [rsp + 128]"); // reload the destination pointer - ctx.emitter.instruction("mov QWORD PTR [rsp + 24], r9"); // set z_stream.next_out + ctx.emitter.instruction(&format!("mov QWORD PTR [rsp + {}], r9", zstream.next_out)); // set z_stream.next_out ctx.emitter.instruction("mov r9, QWORD PTR [rsp + 144]"); // reload the destination capacity - ctx.emitter.instruction("mov DWORD PTR [rsp + 32], r9d"); // set z_stream.avail_out + ctx.emitter.instruction(&format!("mov DWORD PTR [rsp + {}], r9d", zstream.avail_out)); // set z_stream.avail_out ctx.emitter.instruction("mov rdi, rsp"); // pass the z_stream pointer to inflate ctx.emitter.instruction("mov esi, 4"); // request a final inflate pass - ctx.emitter.instruction("call inflate"); // decompress the full input + ctx.emitter.emit_call_c("inflate"); // decompress the full input ctx.emitter.instruction("mov QWORD PTR [rsp + 136], rax"); // save the inflate status code - ctx.emitter.instruction("mov rax, QWORD PTR [rsp + 40]"); // read z_stream.total_out + if ctx.emitter.target.platform == crate::codegen_support::platform::Platform::Windows { + ctx.emitter.instruction(&format!("mov eax, DWORD PTR [rsp + {}]", zstream.total_out)); // LLP64 z_stream.total_out is a u32 + } else { + ctx.emitter.instruction(&format!("mov rax, QWORD PTR [rsp + {}]", zstream.total_out)); // LP64 z_stream.total_out is a u64 + } ctx.emitter.instruction("mov QWORD PTR [rsp + 152], rax"); // save the inflated length across inflateEnd ctx.emitter.instruction("mov rdi, rsp"); // pass the z_stream pointer to inflateEnd - ctx.emitter.instruction("call inflateEnd"); // release zlib's internal inflate state + ctx.emitter.emit_call_c("inflateEnd"); // release zlib's internal inflate state ctx.emitter.instruction("cmp QWORD PTR [rsp + 136], 1"); // check for Z_STREAM_END success ctx.emitter.instruction(&format!("jne {}", fail)); // return false for zlib inflate failures ctx.emitter.instruction("mov rax, QWORD PTR [rsp + 128]"); // return the decompressed string pointer @@ -1399,7 +1463,7 @@ fn lower_gzuncompress_x86_64(ctx: &mut FunctionContext<'_>, ok: &str, after: &st ctx.emitter.instruction("lea rsi, [rsp + 16]"); // pass &destLen as the uncompress in/out length ctx.emitter.instruction("mov rdx, QWORD PTR [rsp + 0]"); // pass the source pointer ctx.emitter.instruction("mov rcx, QWORD PTR [rsp + 8]"); // pass the source length - ctx.emitter.instruction("call uncompress"); // zlib-uncompress the source + ctx.emitter.emit_call_c("uncompress"); // zlib-uncompress the source ctx.emitter.instruction("test rax, rax"); // zero zlib status means success ctx.emitter.instruction(&format!("jz {}", ok)); // load the success result for zero status ctx.emitter.instruction("xor eax, eax"); // use a null pointer as the failure sentinel @@ -1505,8 +1569,6 @@ fn lower_substr_aarch64( let length = expect_operand(inst, 2)?; load_as_int(ctx, length, "substr length")?; ctx.emitter.instruction("mov x3, x0"); // move the explicit substring length into the clamp register - } else { - ctx.emitter.instruction("mov x3, #-1"); // use -1 as the sentinel for an omitted substring length } ctx.emitter.instruction("ldr x0, [sp], #16"); // restore the substring offset after optional length materialization ctx.emitter.instruction("ldp x1, x2, [sp], #16"); // restore the source string pointer and length @@ -1520,13 +1582,20 @@ fn lower_substr_aarch64( ctx.emitter.instruction("csel x0, x2, x0, gt"); // clamp offsets past the end to the source-string length ctx.emitter.instruction("add x1, x1, x0"); // advance the result pointer to the selected substring start ctx.emitter.instruction("sub x2, x2, x0"); // compute the remaining byte length after the selected offset - ctx.emitter.instruction("cmn x3, #1"); // check whether the optional length argument was omitted - ctx.emitter.instruction(&format!("b.eq {}", len_done)); // keep the full remaining tail when no explicit length was provided - ctx.emitter.instruction("cmp x3, #0"); // check whether the requested substring length is negative - ctx.emitter.instruction("csel x3, xzr, x3, lt"); // clamp negative requested lengths to zero - ctx.emitter.instruction("cmp x3, x2"); // compare requested length against the remaining tail length - ctx.emitter.instruction("csel x2, x3, x2, lt"); // shrink the result length when the requested length is shorter - ctx.emitter.label(len_done); + // php reads a negative length as "omit that many bytes from the end of the + // tail", not as "empty". Whether a length was passed is known here, so no + // in-band sentinel is needed — and -1 could never be one, since it is also a + // perfectly ordinary length. + if inst.operands.len() >= 3 { + ctx.emitter.instruction("cmp x3, #0"); // is the requested length negative? + ctx.emitter.instruction(&format!("b.ge {}", len_done)); // a non-negative length only needs the tail clamp + ctx.emitter.instruction("add x3, x2, x3"); // drop that many bytes from the end of the tail + ctx.emitter.instruction("cmp x3, #0"); // did the omission consume the whole tail? + ctx.emitter.instruction("csel x3, xzr, x3, lt"); // an over-long omission yields the empty string + ctx.emitter.label(len_done); + ctx.emitter.instruction("cmp x3, x2"); // compare requested length against the remaining tail length + ctx.emitter.instruction("csel x2, x3, x2, lt"); // shrink the result length when the requested length is shorter + } Ok(()) } @@ -1555,8 +1624,6 @@ fn lower_substr_x86_64( let length = expect_operand(inst, 2)?; load_as_int(ctx, length, "substr length")?; ctx.emitter.instruction("mov rcx, rax"); // move the explicit substring length into the clamp register - } else { - abi::emit_load_int_immediate(ctx.emitter, "rcx", -1); } abi::emit_pop_reg(ctx.emitter, "rax"); abi::emit_pop_reg_pair(ctx.emitter, "rdi", "rsi"); @@ -1571,14 +1638,19 @@ fn lower_substr_x86_64( ctx.emitter.instruction("cmovg rax, rsi"); // clamp offsets past the end to the source-string length ctx.emitter.instruction("add rdi, rax"); // advance the result pointer to the selected substring start ctx.emitter.instruction("sub rsi, rax"); // compute the remaining byte length after the selected offset - ctx.emitter.instruction("cmp rcx, -1"); // check whether the optional length argument was omitted - ctx.emitter.instruction(&format!("je {}", len_done)); // keep the full remaining tail when no explicit length was provided - ctx.emitter.instruction("cmp rcx, 0"); // check whether the requested substring length is negative - ctx.emitter.instruction("mov r8, 0"); // materialize zero for negative length clamping - ctx.emitter.instruction("cmovl rcx, r8"); // clamp negative requested lengths to zero - ctx.emitter.instruction("cmp rcx, rsi"); // compare requested length against the remaining tail length - ctx.emitter.instruction("cmovl rsi, rcx"); // shrink the result length when the requested length is shorter - ctx.emitter.label(len_done); + // See the AArch64 arm: a negative length omits bytes from the end of the tail, + // and the argument count is known at compile time rather than in-band. + if inst.operands.len() >= 3 { + ctx.emitter.instruction("cmp rcx, 0"); // is the requested length negative? + ctx.emitter.instruction(&format!("jge {}", len_done)); // a non-negative length only needs the tail clamp + ctx.emitter.instruction("add rcx, rsi"); // drop that many bytes from the end of the tail + ctx.emitter.instruction("cmp rcx, 0"); // did the omission consume the whole tail? + ctx.emitter.instruction("mov r8, 0"); // materialize zero for clamping + ctx.emitter.instruction("cmovl rcx, r8"); // an over-long omission yields the empty string + ctx.emitter.label(len_done); + ctx.emitter.instruction("cmp rcx, rsi"); // compare requested length against the remaining tail length + ctx.emitter.instruction("cmovl rsi, rcx"); // shrink the result length when the requested length is shorter + } ctx.emitter.instruction("mov rax, rdi"); // return the selected substring pointer in the string result register ctx.emitter.instruction("mov rdx, rsi"); // return the selected substring length in the string result register Ok(()) @@ -2461,7 +2533,7 @@ fn materialize_substr_replace_length_aarch64( load_as_int(ctx, length, "substr_replace length")?; ctx.emitter.instruction("mov x7, x0"); // pass the explicit replacement length to the runtime helper } else { - ctx.emitter.instruction("mov x7, #-1"); // use -1 sentinel so replacement runs through the subject end + abi::emit_load_int_immediate(ctx.emitter, "x7", i64::MAX); // i64::MAX runs through the subject end; -1 is a real php length } Ok(()) } @@ -2476,7 +2548,7 @@ fn materialize_substr_replace_length_x86_64( load_as_int(ctx, length, "substr_replace length")?; ctx.emitter.instruction("mov r8, rax"); // pass the explicit replacement length to the runtime helper } else { - abi::emit_load_int_immediate(ctx.emitter, "r8", -1); + abi::emit_load_int_immediate(ctx.emitter, "r8", i64::MAX); // i64::MAX runs through the subject end; -1 is a real php length } Ok(()) } diff --git a/src/codegen/lower_inst/builtins/system.rs b/src/codegen/lower_inst/builtins/system.rs index dac4073b7d..161bc49f59 100644 --- a/src/codegen/lower_inst/builtins/system.rs +++ b/src/codegen/lower_inst/builtins/system.rs @@ -59,10 +59,28 @@ fn lower_date_like( stage_date_string_regs(ctx); load_date_timestamp(ctx, timestamp)?; unstage_date_string_regs(ctx); + publish_windows_tz_offset_bridge(ctx); abi::emit_call_label(ctx.emitter, runtime_symbol); store_if_result(ctx, inst) } +/// Publishes the windows-only elephc-tz offset bridge fn pointer immediately +/// before a runtime call that may resolve local time on windows +/// (`__rt_date`/`__rt_mktime`/`__rt_gmmktime`/`__rt_strtotime`, transitively +/// `__rt_sys_localtime`/`__rt_sys_mktime`). No-op off windows-x86_64. Shared by +/// `lower_date_like`, `lower_mktime_like`, and `emit_strtotime_marshal` so the +/// windows-only `__rt_sys_localtime`/`__rt_sys_mktime` shims +/// (`codegen_support/runtime/win32/shims_time.rs`) can resolve the active +/// default timezone's IANA offset instead of falling back to msvcrt's +/// TZ-only/no-zoneinfo `localtime`/`mktime`. Harmless to publish before the +/// UTC-only counterparts (`gmdate`/`gmmktime` share this call site / never read +/// the slot) — publishing costs a few extra instructions, not correctness. +fn publish_windows_tz_offset_bridge(ctx: &mut FunctionContext<'_>) { + if ctx.emitter.target.platform == Platform::Windows { + crate::codegen_support::tz_bridge::publish_elephc_tz_offset_function_pointer(ctx.emitter); + } +} + /// Lowers `date_default_timezone_get()` through the shared runtime helper. /// /// Takes no arguments; `__rt_date_default_timezone_get` returns the stored timezone @@ -231,7 +249,7 @@ pub(crate) fn lower_localtime( } emit_store_result_to_scratch(ctx, 8); emit_load_scratch_to_reg(ctx, abi::int_result_reg(ctx.emitter), 0); - emit_load_scratch_to_reg(ctx, abi::int_arg_reg_name(ctx.emitter.target, 1), 8); + emit_load_scratch_to_reg(ctx, abi::runtime_helper_int_arg_reg(ctx.emitter, 1), 8); emit_scratch_release(ctx, 16); abi::emit_call_label(ctx.emitter, "__rt_localtime"); emit_box_hash_pointer_as_assoc_mixed(ctx); @@ -272,7 +290,7 @@ pub(crate) fn lower_http_response_code( } None => abi::emit_load_int_immediate( ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 0), + abi::runtime_helper_int_arg_reg(ctx.emitter, 0), 0, ), } @@ -338,6 +356,11 @@ fn lower_mktime_like( runtime_symbol: &str, ) -> Result<()> { super::ensure_arg_count(inst, name, 6)?; + // Publish BEFORE marshaling the integer args, not after: the publish call's + // r9 scratch use (see `publish_windows_tz_offset_bridge`) would otherwise + // clobber r9d, which is `__rt_mktime`'s/`__rt_gmmktime`'s 6th ABI argument + // (year) once `marshal_integer_args` has loaded it. + publish_windows_tz_offset_bridge(ctx); marshal_integer_args(ctx, inst, &MKTIME_ARG_LABELS)?; abi::emit_call_label(ctx.emitter, runtime_symbol); store_if_result(ctx, inst) @@ -452,7 +475,7 @@ fn emit_store_result_to_scratch(ctx: &mut FunctionContext<'_>, offset: usize) { /// Loads the staged integer at scratch `offset` into the `index`-th integer argument register. fn emit_load_scratch_to_arg_reg(ctx: &mut FunctionContext<'_>, index: usize, offset: usize) { - let arg_reg = abi::int_arg_reg_name(ctx.emitter.target, index); + let arg_reg = abi::runtime_helper_int_arg_reg(ctx.emitter, index); emit_load_scratch_to_reg(ctx, arg_reg, offset); } @@ -585,6 +608,7 @@ fn emit_strtotime_marshal( } } } + publish_windows_tz_offset_bridge(ctx); abi::emit_call_label(ctx.emitter, "__rt_strtotime"); Ok(()) } @@ -745,7 +769,7 @@ fn lower_direct_system_call( if ctx.emitter.target.arch == Arch::X86_64 { ctx.emitter.instruction("mov rdi, rax"); // pass the null-terminated shell command to libc system() } - ctx.emitter.bl_c("system"); + ctx.emitter.emit_call_c("system"); if returns_empty_string { emit_empty_string_result(ctx); } @@ -779,8 +803,15 @@ fn emit_dynamic_exit(ctx: &mut FunctionContext<'_>) { (Platform::MacOS, Arch::X86_64) => { panic!("exit() is not implemented yet for target macos-x86_64"); } - (Platform::Windows, _) => { - panic!("Windows target is not yet supported (see issue #379)"); + (Platform::Windows, Arch::X86_64) => { + ctx.emitter.instruction("mov rbx, rax"); // preserve the computed exit code across output-buffer shutdown + ctx.emitter.instruction("and rsp, -16"); // realign the terminal path before calling runtime helpers + ctx.emitter.instruction("call __rt_ob_flush_all"); // drain still-active output buffers before terminating + ctx.emitter.instruction("mov rdi, rbx"); // restore the exit code into the shim's SysV-style argument register + ctx.emitter.instruction("call __rt_sys_exit"); // call Win32 ExitProcess shim after the shutdown drain + } + (Platform::Windows, Arch::AArch64) => { + panic!("Windows ARM64 target is not yet supported (see issue #379)"); } } } @@ -835,7 +866,7 @@ fn lower_putenv_x86_64(ctx: &mut FunctionContext<'_>) { ctx.emitter.label(©_done); ctx.emitter.instruction("mov BYTE PTR [r9 + r10], 0"); // append the C null terminator required by putenv() ctx.emitter.instruction("mov rdi, r9"); // pass the persistent environment buffer to putenv() - ctx.emitter.bl_c("putenv"); + ctx.emitter.emit_call_c("putenv"); ctx.emitter.instruction("cmp rax, 0"); // compare libc putenv() status against success ctx.emitter.instruction("sete al"); // return true when putenv() accepted the assignment ctx.emitter.instruction("movzx rax, al"); // widen the boolean byte into the integer result register diff --git a/src/codegen/lower_inst/callables.rs b/src/codegen/lower_inst/callables.rs index 7119ad1471..bdd1cd4491 100644 --- a/src/codegen/lower_inst/callables.rs +++ b/src/codegen/lower_inst/callables.rs @@ -551,7 +551,9 @@ fn runtime_builtin_descriptor_cases( candidate_names: Option<&[String]>, ) -> Result> { let mut cases = Vec::new(); - for name in crate::types::checker::builtins::supported_builtin_function_names() { + for name in crate::types::checker::builtins::supported_builtin_function_names_on_platform( + ctx.emitter.target.platform, + ) { if !runtime_callable_name_is_reachable(name, candidate_names) || !callable_dispatch::runtime_builtin_wrapper_supported(name, source_arg_ty) || ctx @@ -2060,7 +2062,7 @@ pub(super) fn emit_descriptor_reg_invoker_mixed_result_with_args( invoker_reg, descriptor_reg, ); - abi::emit_call_reg(ctx.emitter, invoker_reg); + emit_descriptor_invoker_reg_call(ctx.emitter, invoker_reg); release_invoker_arg_preserving_result(ctx); if release_runtime_descriptor { release_saved_runtime_descriptor_preserving_result(ctx); @@ -2144,7 +2146,7 @@ fn emit_descriptor_reg_invoker_mixed_result_with_prebuilt_mixed_arg( invoker_reg, descriptor_reg, ); - abi::emit_call_reg(ctx.emitter, invoker_reg); + emit_descriptor_invoker_reg_call(ctx.emitter, invoker_reg); if release_runtime_descriptor { release_saved_runtime_descriptor_preserving_result(ctx); } @@ -2183,12 +2185,20 @@ fn emit_descriptor_reg_invoker_mixed_result_with_normalized_arg( invoker_reg, descriptor_reg, ); - abi::emit_call_reg(ctx.emitter, invoker_reg); + emit_descriptor_invoker_reg_call(ctx.emitter, invoker_reg); release_invoker_arg_preserving_result(ctx); release_saved_descriptor_after_normalized_arg(ctx, release_runtime_descriptor); Ok(()) } +/// Calls a generated descriptor invoker with the platform-required caller stack area. +fn emit_descriptor_invoker_reg_call(emitter: &mut crate::codegen::emit::Emitter, invoker_reg: &str) { + let call_pad_bytes = abi::outgoing_call_stack_pad_bytes(emitter.target, 0); + abi::emit_reserve_temporary_stack(emitter, call_pad_bytes); + abi::emit_call_reg(emitter, invoker_reg); + abi::emit_release_temporary_stack(emitter, call_pad_bytes); +} + /// Returns the branch-ready label name for a descriptor invoker callsite. fn descriptor_invoker_ready_label(ctx: &mut FunctionContext<'_>, op_name: &str) -> String { if matches!(op_name, "callable_descriptor_invoke" | "iterator_apply") { diff --git a/src/codegen/lower_inst/enums.rs b/src/codegen/lower_inst/enums.rs index 4886b4bf17..ae8ddad89a 100644 --- a/src/codegen/lower_inst/enums.rs +++ b/src/codegen/lower_inst/enums.rs @@ -481,10 +481,7 @@ pub(super) fn lower_enum_backing_string_to_int( abi::emit_jump(ctx.emitter, &done_label); ctx.emitter.label(&type_error_label); abi::emit_release_temporary_stack(ctx.emitter, 16); - match ctx.emitter.target.arch { - Arch::AArch64 => emit_throw_enum_from_type_error_aarch64(ctx, &message_label, message_len), - Arch::X86_64 => emit_throw_enum_from_type_error_x86_64(ctx, &message_label, message_len), - } + emit_throw_static_type_error(ctx, &message_label, message_len); ctx.emitter.label(&done_label); store_if_result(ctx, inst) } @@ -494,7 +491,10 @@ pub(super) fn lower_enum_backing_string_to_int( /// int-result register and control falls through. On a non-numeric string, control branches /// to `invalid_label` with the 16-byte temporary still on the stack — the caller is /// responsible for releasing it and emitting the `TypeError` there. -fn emit_string_result_to_int_checked(ctx: &mut FunctionContext<'_>, invalid_label: &str) { +pub(super) fn emit_string_result_to_int_checked( + ctx: &mut FunctionContext<'_>, + invalid_label: &str, +) { let (string_ptr_reg, string_len_reg) = abi::string_result_regs(ctx.emitter); let int_reg = abi::int_result_reg(ctx.emitter); // Preserve the input string across the numeric-validity probe, which clobbers the @@ -701,8 +701,24 @@ fn emit_throw_type_error_from_string_result(ctx: &mut FunctionContext<'_>) { } } +/// Throws a catchable `TypeError` whose message is stored in static program data. +pub(super) fn emit_throw_static_type_error( + ctx: &mut FunctionContext<'_>, + message_label: &str, + message_len: usize, +) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + emit_throw_static_type_error_aarch64(ctx, message_label, message_len); + } + Arch::X86_64 => { + emit_throw_static_type_error_x86_64(ctx, message_label, message_len); + } + } +} + /// Emits the AArch64 `TypeError` allocation, static-message stamping, and unwinder handoff. -fn emit_throw_enum_from_type_error_aarch64( +fn emit_throw_static_type_error_aarch64( ctx: &mut FunctionContext<'_>, message_label: &str, message_len: usize, @@ -724,7 +740,7 @@ fn emit_throw_enum_from_type_error_aarch64( } /// Emits the x86_64 `TypeError` allocation, static-message stamping, and unwinder handoff. -fn emit_throw_enum_from_type_error_x86_64( +fn emit_throw_static_type_error_x86_64( ctx: &mut FunctionContext<'_>, message_label: &str, message_len: usize, diff --git a/src/codegen/lower_inst/externs.rs b/src/codegen/lower_inst/externs.rs index 735b6ab490..0982ee0309 100644 --- a/src/codegen/lower_inst/externs.rs +++ b/src/codegen/lower_inst/externs.rs @@ -74,10 +74,14 @@ pub(super) fn lower_extern_call(ctx: &mut FunctionContext<'_>, inst: &Instructio } let assignments = - abi::build_outgoing_arg_assignments_for_target(ctx.emitter.target, &c_param_types, 0); + abi::build_c_abi_outgoing_arg_assignments_for_target(ctx.emitter.target, &c_param_types); let overflow_bytes = abi::materialize_outgoing_args(ctx.emitter, &assignments); + abi::compact_windows_c_abi_stack_args(ctx.emitter, &assignments); + let call_pad_bytes = abi::outgoing_call_stack_pad_bytes(ctx.emitter.target, 0); + abi::emit_reserve_temporary_stack(ctx.emitter, call_pad_bytes); let symbol = ctx.emitter.target.extern_symbol(&decl.name); abi::emit_call_label(ctx.emitter, &symbol); + abi::emit_release_temporary_stack(ctx.emitter, call_pad_bytes); abi::emit_release_temporary_stack(ctx.emitter, overflow_bytes); normalize_extern_return(ctx, &decl.return_php_type)?; release_borrowed_cstr_temps(ctx, string_arg_count, cleanup_bytes, &decl.return_php_type); diff --git a/src/codegen/lower_inst/floats.rs b/src/codegen/lower_inst/floats.rs index c9b9b8db77..12c5a62784 100644 --- a/src/codegen/lower_inst/floats.rs +++ b/src/codegen/lower_inst/floats.rs @@ -129,12 +129,12 @@ pub(super) fn lower_float_pow(ctx: &mut FunctionContext<'_>, inst: &Instruction) Arch::AArch64 => { require_float(ctx.load_value_to_reg(lhs, "d0")?, inst)?; require_float(ctx.load_value_to_reg(rhs, "d1")?, inst)?; - ctx.emitter.bl_c("pow"); + ctx.emitter.emit_call_c("pow"); } Arch::X86_64 => { require_float(ctx.load_value_to_reg(lhs, "xmm0")?, inst)?; require_float(ctx.load_value_to_reg(rhs, "xmm1")?, inst)?; - ctx.emitter.instruction("call pow"); // compute floating-point exponentiation through libc pow() + ctx.emitter.emit_call_c("pow"); // compute floating-point exponentiation through libc pow() (Windows-safe: shadow space via __rt_sys_pow) } } store_if_result(ctx, inst) diff --git a/src/codegen/lower_inst/iterators.rs b/src/codegen/lower_inst/iterators.rs index 52800c257b..813b75aef7 100644 --- a/src/codegen/lower_inst/iterators.rs +++ b/src/codegen/lower_inst/iterators.rs @@ -1018,6 +1018,10 @@ fn emit_object_iterator_method_call( let caller_stack_pad_bytes = direct_call_stack_pad_bytes(ctx, overflow_bytes); abi::emit_reserve_temporary_stack(ctx.emitter, caller_stack_pad_bytes); if let Some(helper) = target.runtime_helper { + super::remap_platform_args_to_runtime_helper_regs( + ctx.emitter, + super::int_register_arg_count(&[PhpType::Object(class_name.to_string())]), + ); abi::emit_call_label(ctx.emitter, helper); } else { abi::emit_call_label(ctx.emitter, &method_symbol(&target.impl_class, &method_key)); @@ -1034,6 +1038,8 @@ fn emit_interface_iterator_method_call( interface_name: &str, method_name: &str, ) -> Result { + // Platform receiver register (rdi SysV, rcx MSx64) — feeds emit_interface_dispatch_call's + // scan and, via emit_generator_interface_fast_path's own remap, the SysV __rt_gen_* helper. let receiver_arg = abi::int_arg_reg_name(ctx.emitter.target, 0); abi::load_at_offset(ctx.emitter, receiver_arg, offset - ITER_SOURCE_OFFSET_DELTA); let method_key = php_symbol_key(method_name); @@ -1069,10 +1075,14 @@ fn emit_generator_interface_fast_path( ctx.emitter.label(¬_generator); } Arch::X86_64 => { - ctx.emitter.instruction("mov r10, QWORD PTR [rdi]"); // load receiver class id before checking for the built-in Generator + // Platform receiver register (rdi SysV, rcx MSx64) — matches how the caller staged $this. + let receiver_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); + ctx.emitter + .instruction(&format!("mov r10, QWORD PTR [{}]", receiver_reg)); // load receiver class id before checking for the built-in Generator abi::emit_load_symbol_to_reg(ctx.emitter, "r11", "_generator_class_id", 0); ctx.emitter.instruction("cmp r10, r11"); // compare receiver class id with Generator ctx.emitter.instruction(&format!("jne {}", not_generator)); // fall back to interface dispatch for non-Generator iterators + super::remap_platform_args_to_runtime_helper_regs(ctx.emitter, 1); // MSx64 receiver -> SysV receiver for the hand-written __rt_gen_* helper abi::emit_call_label(ctx.emitter, helper); ctx.emitter.instruction(&format!("jmp {}", done)); // skip generic interface dispatch after the fast path ctx.emitter.label(¬_generator); @@ -1138,17 +1148,27 @@ pub(super) fn emit_interface_dispatch_call( ctx.emitter.instruction("mov x0, #0"); // defensive fallback for invalid interface metadata } Arch::X86_64 => { - ctx.emitter.instruction("mov r10, QWORD PTR [rdi]"); // load receiver class id for interface metadata lookup + // Platform receiver register (rdi SysV, rcx MSx64) — the resolved callee reads $this from the same register. + let receiver_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); + ctx.emitter + .instruction(&format!("mov r10, QWORD PTR [{}]", receiver_reg)); // load receiver class id for interface metadata lookup abi::emit_symbol_address(ctx.emitter, "r11", "_class_interface_ptrs"); ctx.emitter.instruction("mov r11, QWORD PTR [r11 + r10 * 8]"); // select this class's interface metadata block ctx.emitter.instruction("mov r10, QWORD PTR [r11]"); // load implemented interface count ctx.emitter.instruction("add r11, 8"); // move to first [interface id, table] pair - abi::emit_load_int_immediate(ctx.emitter, "r9", interface_id); ctx.emitter.label(&scan_loop); ctx.emitter.instruction("test r10, r10"); // check whether implemented interfaces remain ctx.emitter.instruction(&format!("je {}", missing)); // stop when no implemented interface matched - ctx.emitter.instruction("mov r8, QWORD PTR [r11]"); // load current implemented interface id - ctx.emitter.instruction("cmp r8, r9"); // compare with target interface id + // Compare the current entry's interface id directly against the target as a + // memory-immediate `cmp`, instead of loading each side into r8/r9: those are live + // MSx64 arg2/arg3 registers for THIS call's already-staged arguments (e.g. the + // pointer/length pair of a `format($fmt)`-style string argument) on windows-x86_64, + // and SysV arg5/arg6 on linux/macos-x86_64 for a 5+ arg interface call — clobbering + // either silently corrupts an argument the resolved callee reads right after `call + // r11` below (surfaced as a truncated/garbled `DateTimeInterface::format()` result + // when chaining a call through an interface-typed receiver, e.g. + // `$period->getStartDate()->format($fmt)`). + ctx.emitter.instruction(&format!("cmp QWORD PTR [r11], {}", interface_id)); // compare current implemented interface id with target ctx.emitter.instruction(&format!("je {}", found)); // dispatch through this table when matched ctx.emitter.instruction("add r11, 16"); // advance to next interface metadata entry ctx.emitter.instruction("sub r10, 1"); // consume one interface entry @@ -1202,7 +1222,7 @@ fn emit_generator_iterator_runtime_call( offset: usize, helper: &str, ) { - let receiver_arg = abi::int_arg_reg_name(ctx.emitter.target, 0); + let receiver_arg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); abi::load_at_offset(ctx.emitter, receiver_arg, offset - ITER_SOURCE_OFFSET_DELTA); abi::emit_call_label(ctx.emitter, helper); } diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index 67a9749f28..83f68e7a31 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -366,7 +366,7 @@ fn lower_spl_doubly_linked_list_new( .ok_or_else(|| CodegenIrError::unsupported(format!("unknown class {}", class_name)))?; abi::emit_load_int_immediate( ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 0), + abi::runtime_helper_int_arg_reg(ctx.emitter, 0), class_id as i64, ); abi::emit_call_label(ctx.emitter, "__rt_spl_dll_new"); @@ -392,17 +392,17 @@ fn lower_spl_fixed_array_new(ctx: &mut FunctionContext<'_>, inst: &Instruction) abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); abi::emit_load_int_immediate( ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 0), + abi::runtime_helper_int_arg_reg(ctx.emitter, 0), class_id as i64, ); - abi::emit_pop_reg(ctx.emitter, abi::int_arg_reg_name(ctx.emitter.target, 1)); + abi::emit_pop_reg(ctx.emitter, abi::runtime_helper_int_arg_reg(ctx.emitter, 1)); } else { abi::emit_load_int_immediate( ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 0), + abi::runtime_helper_int_arg_reg(ctx.emitter, 0), class_id as i64, ); - abi::emit_load_int_immediate(ctx.emitter, abi::int_arg_reg_name(ctx.emitter.target, 1), 0); + abi::emit_load_int_immediate(ctx.emitter, abi::runtime_helper_int_arg_reg(ctx.emitter, 1), 0); } abi::emit_call_label(ctx.emitter, "__rt_spl_fixed_new"); store_if_result(ctx, inst) @@ -880,10 +880,18 @@ fn emit_branch_if_saved_traversable_implements( } /// Moves the object result into the receiver ABI slot before an interface method call. +/// Targets the platform receiver register (rdi SysV, rcx MSx64) so the resolved interface +/// method — a codegen-emitted callee that reads $this per-target — finds it where expected. +/// No-op on AArch64 and on non-Windows x86_64, where the result register already IS the +/// receiver register. fn move_result_to_receiver_arg(ctx: &mut FunctionContext<'_>) { - if ctx.emitter.target.arch == Arch::X86_64 { - ctx.emitter.instruction("mov rdi, rax"); // pass the normalized object result as the method receiver + let result_reg = abi::int_result_reg(ctx.emitter); + let arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); + if result_reg == arg_reg { + return; } + ctx.emitter + .instruction(&format!("mov {}, {}", arg_reg, result_reg)); // pass the normalized object result as the method receiver } /// Writes the normalized Iterator pointer into IteratorIterator::$inner. @@ -1208,7 +1216,7 @@ fn lower_fiber_new(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result< .get("Fiber") .map(|class| class.class_id) .unwrap_or(0); - let callable_arg = abi::int_arg_reg_name(ctx.emitter.target, 0); + let callable_arg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); if let Some(callable) = inst.operands.first().copied() { let callable_ty = ctx.value_php_type(callable)?.codegen_repr(); if callable_ty == PhpType::Str { @@ -1248,10 +1256,10 @@ fn lower_fiber_new(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result< } abi::emit_load_int_immediate( ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), + abi::runtime_helper_int_arg_reg(ctx.emitter, 1), class_id as i64, ); - let wrapper_arg = abi::int_arg_reg_name(ctx.emitter.target, 2); + let wrapper_arg = abi::runtime_helper_int_arg_reg(ctx.emitter, 2); if let Some(wrapper) = fibers::wrapper_for_fiber_new(ctx.module, ctx.function, inst) { abi::emit_symbol_address(ctx.emitter, wrapper_arg, &wrapper.label); } else { @@ -1778,17 +1786,17 @@ fn emit_dynamic_new_mixed_spl_fixed_array_candidate( abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); abi::emit_load_int_immediate( ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 0), + abi::runtime_helper_int_arg_reg(ctx.emitter, 0), class_id as i64, ); - abi::emit_pop_reg(ctx.emitter, abi::int_arg_reg_name(ctx.emitter.target, 1)); + abi::emit_pop_reg(ctx.emitter, abi::runtime_helper_int_arg_reg(ctx.emitter, 1)); } else { abi::emit_load_int_immediate( ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 0), + abi::runtime_helper_int_arg_reg(ctx.emitter, 0), class_id as i64, ); - abi::emit_load_int_immediate(ctx.emitter, abi::int_arg_reg_name(ctx.emitter.target, 1), 0); + abi::emit_load_int_immediate(ctx.emitter, abi::runtime_helper_int_arg_reg(ctx.emitter, 1), 0); } abi::emit_call_label(ctx.emitter, "__rt_spl_fixed_new"); emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Object("SplFixedArray".to_string())); @@ -1803,7 +1811,7 @@ fn emit_dynamic_new_mixed_spl_dll_candidate( ) -> Result<()> { abi::emit_load_int_immediate( ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 0), + abi::runtime_helper_int_arg_reg(ctx.emitter, 0), class_id as i64, ); abi::emit_call_label(ctx.emitter, "__rt_spl_dll_new"); @@ -2778,18 +2786,12 @@ fn emit_magic_get_args( property: &str, ) -> Result<()> { let (label, len) = ctx.data.add_string(property.as_bytes()); - match ctx.emitter.target.arch { - Arch::AArch64 => { - ctx.load_value_to_reg(object, "x0")?; - abi::emit_symbol_address(ctx.emitter, "x1", &label); - abi::emit_load_int_immediate(ctx.emitter, "x2", len as i64); - } - Arch::X86_64 => { - ctx.load_value_to_reg(object, "rdi")?; - abi::emit_symbol_address(ctx.emitter, "rsi", &label); - abi::emit_load_int_immediate(ctx.emitter, "rdx", len as i64); - } - } + let receiver_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); + let name_ptr_reg = abi::int_arg_reg_name(ctx.emitter.target, 1); + let name_len_reg = abi::int_arg_reg_name(ctx.emitter.target, 2); + ctx.load_value_to_reg(object, receiver_reg)?; + abi::emit_symbol_address(ctx.emitter, name_ptr_reg, &label); + abi::emit_load_int_immediate(ctx.emitter, name_len_reg, len as i64); Ok(()) } @@ -4385,11 +4387,11 @@ pub(super) fn lower_instanceof(ctx: &mut FunctionContext<'_>, inst: &Instruction }; match value_ty { PhpType::Object(_) => { - ctx.load_value_to_reg(value, abi::int_arg_reg_name(ctx.emitter.target, 0))?; + ctx.load_value_to_reg(value, abi::runtime_helper_int_arg_reg(ctx.emitter, 0))?; emit_match_call(ctx, target_id, target_kind, "__rt_exception_matches"); } PhpType::Mixed | PhpType::Union(_) => { - ctx.load_value_to_reg(value, abi::int_arg_reg_name(ctx.emitter.target, 0))?; + ctx.load_value_to_reg(value, abi::runtime_helper_int_arg_reg(ctx.emitter, 0))?; emit_match_call(ctx, target_id, target_kind, "__rt_mixed_instanceof"); } _ => emit_false(ctx), @@ -4604,7 +4606,7 @@ fn emit_clone_dynamic_property_hash( } abi::emit_push_reg(ctx.emitter, source_reg); abi::emit_push_reg(ctx.emitter, dest_reg); - let hash_arg = abi::int_arg_reg_name(ctx.emitter.target, 0); + let hash_arg = abi::runtime_helper_int_arg_reg(ctx.emitter, 0); if hash_arg != result_reg { abi::emit_reg_move(ctx.emitter, hash_arg, result_reg); } @@ -6229,6 +6231,7 @@ fn emit_normalized_dynamic_instanceof_value( Ok(()) } +/// Converts a boxed Mixed object payload to its object pointer and every other tag to null. fn emit_mixed_instanceof_value_normalization(ctx: &mut FunctionContext<'_>) { let object_label = ctx.next_label("instanceof_dynamic_value_object"); let done = ctx.next_label("instanceof_dynamic_value_done"); @@ -6370,9 +6373,9 @@ fn emit_dynamic_match_call(ctx: &mut FunctionContext<'_>) { abi::emit_push_reg(ctx.emitter, "rdx"); } } - abi::emit_pop_reg(ctx.emitter, abi::int_arg_reg_name(ctx.emitter.target, 2)); - abi::emit_pop_reg(ctx.emitter, abi::int_arg_reg_name(ctx.emitter.target, 1)); - abi::emit_pop_reg(ctx.emitter, abi::int_arg_reg_name(ctx.emitter.target, 0)); + abi::emit_pop_reg(ctx.emitter, abi::runtime_helper_int_arg_reg(ctx.emitter, 2)); + abi::emit_pop_reg(ctx.emitter, abi::runtime_helper_int_arg_reg(ctx.emitter, 1)); + abi::emit_pop_reg(ctx.emitter, abi::runtime_helper_int_arg_reg(ctx.emitter, 0)); abi::emit_call_label(ctx.emitter, "__rt_exception_matches"); } @@ -6385,12 +6388,12 @@ fn emit_invalid_dynamic_target_fatal(ctx: &mut FunctionContext<'_>) { fn emit_match_call(ctx: &mut FunctionContext<'_>, target_id: u64, target_kind: i64, helper: &str) { abi::emit_load_int_immediate( ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 1), + abi::runtime_helper_int_arg_reg(ctx.emitter, 1), target_id as i64, ); abi::emit_load_int_immediate( ctx.emitter, - abi::int_arg_reg_name(ctx.emitter.target, 2), + abi::runtime_helper_int_arg_reg(ctx.emitter, 2), target_kind, ); abi::emit_call_label(ctx.emitter, helper); diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 3a553c43e4..90b8c35efb 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -779,7 +779,7 @@ fn emit_reflection_owner_object( } } if matches!(class_name, "ReflectionFunction" | "ReflectionMethod") { - let is_internal = reflection_function_or_method_is_internal(class_name, &metadata); + let is_internal = reflection_function_or_method_is_internal(ctx, class_name, &metadata); emit_reflection_owner_bool_property(ctx, class_name, "__is_internal", is_internal)?; emit_reflection_owner_bool_property( ctx, @@ -1361,7 +1361,7 @@ fn reflection_function_metadata( let function_name = const_required_string_operand(ctx, function_operand, "ReflectionFunction")?; let Some(function) = ctx.function_by_name(&function_name) else { if let Some((builtin_name, signature)) = - reflection_builtin_function_signature(&function_name) + reflection_builtin_function_signature(ctx, &function_name) { return reflection_builtin_function_metadata(ctx, &builtin_name, &signature); } @@ -1438,14 +1438,24 @@ fn reflection_builtin_function_metadata( } /// Returns the canonical callable-builtin name and signature for ReflectionFunction. -fn reflection_builtin_function_signature(function_name: &str) -> Option<(String, FunctionSig)> { +fn reflection_builtin_function_signature( + ctx: &FunctionContext<'_>, + function_name: &str, +) -> Option<(String, FunctionSig)> { let builtin_key = php_symbol_key(function_name.trim_start_matches('\\')); + if !crate::types::checker::builtins::builtin_available_on_platform( + &builtin_key, + ctx.emitter.target.platform, + ) { + return None; + } crate::types::first_class_callable_builtin_sig(&builtin_key) .map(|signature| (builtin_key, signature)) } /// Returns whether a reflected function or method represents compiler builtin metadata. fn reflection_function_or_method_is_internal( + ctx: &FunctionContext<'_>, class_name: &str, metadata: &ReflectionOwnerMetadata, ) -> bool { @@ -1453,7 +1463,7 @@ fn reflection_function_or_method_is_internal( return metadata .reflected_name .as_deref() - .and_then(reflection_builtin_function_signature) + .and_then(|name| reflection_builtin_function_signature(ctx, name)) .is_some(); } metadata @@ -1684,7 +1694,7 @@ fn reflection_function_parameter_metadata( let selector = const_parameter_selector_operand(ctx, parameter_operand)?; let Some(function) = ctx.function_by_name(&function_name) else { if let Some((builtin_name, signature)) = - reflection_builtin_function_signature(&function_name) + reflection_builtin_function_signature(ctx, &function_name) { let metadata = reflection_builtin_function_metadata(ctx, &builtin_name, &signature)?; let Some(parameter) = diff --git a/src/codegen/lower_inst/ownership.rs b/src/codegen/lower_inst/ownership.rs index 171d2ed974..2c299380ab 100644 --- a/src/codegen/lower_inst/ownership.rs +++ b/src/codegen/lower_inst/ownership.rs @@ -91,6 +91,41 @@ pub(super) fn lower_release(ctx: &mut FunctionContext<'_>, inst: &Instruction) - Ok(()) } +/// Returns whether a runtime call hands back storage the caller may have to release. +/// +/// `fread()` returns a slice of the 64 KiB concat arena while the read fits in it, and +/// an owned `__rt_heap_alloc` block when it does not. `sys_get_temp_dir()` splits the +/// same way by target rather than by size: the Windows arm hands back an owned block +/// holding the converted path, where the POSIX arm returns borrowed storage. Either +/// way the caller cannot know at compile time which it got, so neither may be skipped +/// here. `__rt_heap_free_safe` then tells them apart exactly rather than +/// heuristically: `_concat_buf` and `_heap_buf` are separate `.comm` objects, so an +/// arena pointer can never satisfy its managed-heap window test and passes through +/// untouched, leaving only the owned block to be reclaimed. +/// +/// Their `result_ownership()` stays `MayAliasArguments` deliberately. Calling them +/// `Fresh` would be untrue — the borrowed case is not owned storage, and other passes +/// read that contract — whereas this predicate only decides whether a release is +/// emitted, which is safe to answer conservatively. None of them takes a string +/// argument, so the aliasing the conservative classification guards against cannot +/// arise for them. +/// +/// Without this, the release the EIR already emits for these values was dropped in the +/// backend, and every such call leaked its block for the life of the process. +/// +/// `stream_get_contents()` is deliberately NOT listed. It would be unreachable here: +/// its checker signature is the union `string|false`, so the value is never +/// `PhpType::Str` and [`value_is_scratch_string`] returns before this predicate is +/// consulted. Its release always takes the `__rt_decref_mixed` arm instead, and the +/// block its accumulation buffer strands is a separate defect that lives in the +/// boxing path — see the note in `io/fread.rs`. +fn returns_arena_slice_or_owned_block(target: crate::ir::RuntimeFnId) -> bool { + matches!( + target, + crate::ir::RuntimeFnId::Fread | crate::ir::RuntimeFnId::SysGetTempDir + ) +} + /// Returns whether a value is a transient string backed by concat scratch storage. fn value_is_scratch_string(ctx: &FunctionContext<'_>, value: ValueId) -> Result { if ctx.value_php_type(value)? != PhpType::Str { @@ -114,10 +149,12 @@ fn value_is_scratch_string(ctx: &FunctionContext<'_>, value: ValueId) -> Result< )) => false, Some(crate::ir::Immediate::RuntimeCall( crate::ir::RuntimeCallTarget::Function(target), - )) => matches!( - target.result_ownership(), - crate::builtins::semantics::BuiltinResultOwnership::Fresh - ), + )) => { + matches!( + target.result_ownership(), + crate::builtins::semantics::BuiltinResultOwnership::Fresh + ) || returns_arena_slice_or_owned_block(target) + } Some(crate::ir::Immediate::RuntimeCall( crate::ir::RuntimeCallTarget::UnaryString(_), )) => true, diff --git a/src/codegen/lower_inst/runtime_calls.rs b/src/codegen/lower_inst/runtime_calls.rs index f67df49388..6ed6e42428 100644 --- a/src/codegen/lower_inst/runtime_calls.rs +++ b/src/codegen/lower_inst/runtime_calls.rs @@ -65,6 +65,8 @@ fn unary_string_symbol(runtime: UnaryStringRuntime) -> &'static str { UnaryStringRuntime::Base64Decode => "__rt_base64_decode", UnaryStringRuntime::Base64Encode => "__rt_base64_encode", UnaryStringRuntime::BinToHex => "__rt_bin2hex", + UnaryStringRuntime::EscapeShellArg => "__rt_escapeshellarg", + UnaryStringRuntime::EscapeShellCmd => "__rt_escapeshellcmd", UnaryStringRuntime::HexToBin => "__rt_hex2bin", UnaryStringRuntime::HtmlEntityDecode => "__rt_html_entity_decode", UnaryStringRuntime::NlToBr => "__rt_nl2br", diff --git a/src/codegen/lower_inst/runtime_functions/group_05.rs b/src/codegen/lower_inst/runtime_functions/group_05.rs index 1e531002ce..93aaa1d2b0 100644 --- a/src/codegen/lower_inst/runtime_functions/group_05.rs +++ b/src/codegen/lower_inst/runtime_functions/group_05.rs @@ -43,6 +43,18 @@ pub(super) fn lower( RuntimeFnId::Popen => Some({ crate::codegen::lower_inst::builtins::io::lower_popen(ctx, inst) }), + RuntimeFnId::ProcClose => Some({ + crate::codegen::lower_inst::builtins::io::lower_proc_close(ctx, inst) + }), + RuntimeFnId::ProcGetStatus => Some({ + crate::codegen::lower_inst::builtins::io::lower_proc_get_status(ctx, inst) + }), + RuntimeFnId::ProcOpen => Some({ + crate::codegen::lower_inst::builtins::io::lower_proc_open(ctx, inst) + }), + RuntimeFnId::ProcTerminate => Some({ + crate::codegen::lower_inst::builtins::io::lower_proc_terminate(ctx, inst) + }), RuntimeFnId::PrintR => Some({ crate::codegen::lower_inst::builtins::debug::lower_print_r(ctx, inst) }), diff --git a/src/codegen/lower_inst/runtime_functions/group_07.rs b/src/codegen/lower_inst/runtime_functions/group_07.rs index 53230fcb81..de1ef88997 100644 --- a/src/codegen/lower_inst/runtime_functions/group_07.rs +++ b/src/codegen/lower_inst/runtime_functions/group_07.rs @@ -112,6 +112,9 @@ pub(super) fn lower( RuntimeFnId::Rand => Some({ crate::codegen::lower_inst::builtins::math::lower_rand(ctx, inst, "rand") }), + RuntimeFnId::RandomBytes => Some({ + crate::codegen::lower_inst::builtins::math::lower_random_bytes(ctx, inst) + }), RuntimeFnId::RandomInt => Some({ crate::codegen::lower_inst::builtins::math::lower_random_int(ctx, inst) }), diff --git a/src/codegen/lower_inst/strings.rs b/src/codegen/lower_inst/strings.rs index 3d1896ea90..b148f50ab1 100644 --- a/src/codegen/lower_inst/strings.rs +++ b/src/codegen/lower_inst/strings.rs @@ -163,11 +163,28 @@ fn emit_eval_native_frame_called_class_override_probe_x86_64( ) { let no_override = ctx.next_label("static_class_no_eval_override"); let (class_label, class_len) = ctx.data.add_string(frame_class.as_bytes()); - abi::emit_reserve_temporary_stack(ctx.emitter, 32); - abi::emit_symbol_address(ctx.emitter, "rdi", &class_label); - abi::emit_load_int_immediate(ctx.emitter, "rsi", class_len as i64); - abi::emit_temporary_stack_address(ctx.emitter, "rdx", 0); - abi::emit_temporary_stack_address(ctx.emitter, "rcx", 8); + let call_pad = abi::outgoing_call_stack_pad_bytes(ctx.emitter.target, 0); + abi::emit_reserve_temporary_stack(ctx.emitter, 32 + call_pad); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 0), + &class_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + class_len as i64, + ); + abi::emit_temporary_stack_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + call_pad, + ); + abi::emit_temporary_stack_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + call_pad + 8, + ); let symbol = ctx .emitter .target @@ -175,12 +192,12 @@ fn emit_eval_native_frame_called_class_override_probe_x86_64( abi::emit_call_label(ctx.emitter, &symbol); ctx.emitter.instruction("cmp rax, 0"); // check whether eval has a late-static class override for this AOT frame ctx.emitter.instruction(&format!("je {}", no_override)); // fall back to the emitted class-id metadata when no override is active - ctx.emitter.instruction("mov rax, QWORD PTR [rsp]"); // load the eval called-class name pointer into the string result - ctx.emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // load the eval called-class name length into the string result - abi::emit_release_temporary_stack(ctx.emitter, 32); + abi::emit_load_temporary_stack_slot(ctx.emitter, "rax", call_pad); + abi::emit_load_temporary_stack_slot(ctx.emitter, "rdx", call_pad + 8); + abi::emit_release_temporary_stack(ctx.emitter, 32 + call_pad); ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the AOT class-id metadata path after using the eval override ctx.emitter.label(&no_override); - abi::emit_release_temporary_stack(ctx.emitter, 32); + abi::emit_release_temporary_stack(ctx.emitter, 32 + call_pad); } /// Returns the generated/AOT class encoded in the current method frame name. diff --git a/src/codegen/lower_term.rs b/src/codegen/lower_term.rs index a1cc783f8d..fa71ac0b71 100644 --- a/src/codegen/lower_term.rs +++ b/src/codegen/lower_term.rs @@ -88,8 +88,19 @@ pub(super) fn lower_terminator(ctx: &mut FunctionContext<'_>, term: &Terminator) let else_label = ctx.block_label_for_id(*else_target)?; let then_edge = edge_label(ctx, then_args, &then_label, "cond_then_args"); let else_edge = edge_label(ctx, else_args, &else_label, "cond_else_args"); - abi::emit_branch_if_int_result_nonzero(ctx.emitter, &then_edge); - abi::emit_jump(ctx.emitter, &else_edge); + match ctx.emitter.target.arch { + Arch::AArch64 => { + let else_landing = ctx.next_label("cond_else_landing"); + abi::emit_branch_if_int_result_zero(ctx.emitter, &else_landing); + abi::emit_jump(ctx.emitter, &then_edge); + ctx.emitter.label(&else_landing); + abi::emit_jump(ctx.emitter, &else_edge); + } + Arch::X86_64 => { + abi::emit_branch_if_int_result_nonzero(ctx.emitter, &then_edge); + abi::emit_jump(ctx.emitter, &else_edge); + } + } emit_edge_args(ctx, &then_edge, *then_target, then_args, "cond_br then")?; emit_edge_args(ctx, &else_edge, *else_target, else_args, "cond_br else")?; Ok(()) @@ -194,8 +205,11 @@ fn lower_switch( abi::emit_load_int_immediate(ctx.emitter, case_reg, case.value); match ctx.emitter.target.arch { Arch::AArch64 => { + let miss_label = ctx.next_label("switch_case_miss"); ctx.emitter.instruction(&format!("cmp {}, {}", result_reg, case_reg)); // compare switch scrutinee with the case value - ctx.emitter.instruction(&format!("b.eq {}", branch_label)); // branch to the matching switch case + ctx.emitter.instruction(&format!("b.ne {}", miss_label)); // keep the conditional edge within its limited AArch64 branch range + abi::emit_jump(ctx.emitter, &branch_label); + ctx.emitter.label(&miss_label); } Arch::X86_64 => { ctx.emitter.instruction(&format!("cmp {}, {}", result_reg, case_reg)); // compare switch scrutinee with the case value @@ -401,13 +415,56 @@ mod tests { assert!(asm.contains("_eir_main_cond_else_args_1:"), "{asm}"); } + /// Verifies AArch64 conditional terminators only use their range-limited + /// branch for a nearby landing pad before taking unrestricted block edges. + #[test] + fn aarch64_cond_br_uses_near_landing_pad() { + let asm = generate_cond_branch_arg_main_asm(Target::new(Platform::Linux, Arch::AArch64)); + + assert!( + asm.contains("cbz x0, _eir_main_cond_else_landing_2"), + "{asm}" + ); + assert!( + asm.contains("b _eir_main_cond_then_args_0") + && asm.contains("_eir_main_cond_else_landing_2:") + && asm.contains("b _eir_main_cond_else_args_1"), + "{asm}" + ); + assert!( + !asm.contains("cbnz x0, _eir_main_cond_then_args_0"), + "{asm}" + ); + } + /// Verifies switch case and default arguments use per-edge copy stubs. #[test] fn switch_arguments_emit_edge_copy_stubs() { let asm = generate_switch_arg_main_asm(Target::new(Platform::Linux, Arch::AArch64)); assert!(asm.contains("_eir_main_switch_case_args_0:"), "{asm}"); - assert!(asm.contains("_eir_main_switch_default_args_1:"), "{asm}"); + assert!(asm.contains("_eir_main_switch_default_args_2:"), "{asm}"); + } + + /// Verifies AArch64 switch comparisons branch locally on misses before + /// taking an unrestricted jump to the matching case edge. + #[test] + fn aarch64_switch_uses_near_miss_landing_pad() { + let asm = generate_switch_arg_main_asm(Target::new(Platform::Linux, Arch::AArch64)); + + assert!( + asm.contains("b.ne _eir_main_switch_case_miss_1"), + "{asm}" + ); + assert!( + asm.contains("b _eir_main_switch_case_args_0") + && asm.contains("_eir_main_switch_case_miss_1:"), + "{asm}" + ); + assert!( + !asm.contains("b.eq _eir_main_switch_case_args_0"), + "{asm}" + ); } /// Verifies throw terminators publish `_exc_value` and call the exception unwinder. diff --git a/src/codegen/runtime_callable_invoker.rs b/src/codegen/runtime_callable_invoker.rs index 676fd92d9c..791d901e99 100644 --- a/src/codegen/runtime_callable_invoker.rs +++ b/src/codegen/runtime_callable_invoker.rs @@ -219,14 +219,12 @@ fn emit_runtime_callable_invoker_impl( /// Loads the descriptor entry slot from the first invoker argument into `call_reg`. fn emit_descriptor_entry_to_call_reg(emitter: &mut Emitter, call_reg: &str) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov {}, x0", call_reg)); // keep descriptor while loading its native entry - } - Arch::X86_64 => { - emitter.instruction(&format!("mov {}, rdi", call_reg)); // keep descriptor while loading its native entry - } - } + // The invoker receives the descriptor in its first ABI argument register, which is + // the target's user-call ABI (rdi on Linux x86_64, rcx on Windows x86_64, x0 on + // AArch64). Reading a hardcoded `rdi` would lose the descriptor on Windows, where + // the caller materializes it in `rcx`. + let descriptor_reg = abi::int_arg_reg_name(emitter.target, 0); + emitter.instruction(&format!("mov {}, {}", call_reg, descriptor_reg)); // keep the descriptor pointer while loading the target entry callable_descriptor::emit_load_entry_from_descriptor(emitter, call_reg, call_reg); } @@ -1355,8 +1353,8 @@ fn emit_hash_lookup_for_param_or_index( ctx: &mut InvokerEmitContext, 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 key_ptr_reg = abi::runtime_helper_int_arg_reg(emitter, 1); + let key_len_reg = abi::runtime_helper_int_arg_reg(emitter, 2); let found_label = param_name.map(|_| ctx.next_label("invoker_assoc_key_found")); if let Some(name) = param_name { @@ -1792,10 +1790,10 @@ fn emit_null_default_to_result(emitter: &mut Emitter, target_ty: Option<&PhpType /// Emits an empty indexed array of `elem_ty` into the integer result register. fn emit_empty_indexed_array(emitter: &mut Emitter, elem_ty: &PhpType) { - abi::emit_load_int_immediate(emitter, abi::int_arg_reg_name(emitter.target, 0), 4); + abi::emit_load_int_immediate(emitter, abi::runtime_helper_int_arg_reg(emitter, 0), 4); abi::emit_load_int_immediate( emitter, - abi::int_arg_reg_name(emitter.target, 1), + abi::runtime_helper_int_arg_reg(emitter, 1), elem_ty.stack_size() as i64, ); abi::emit_call_label(emitter, "__rt_array_new"); @@ -2061,7 +2059,16 @@ fn call_target_with_pushed_args( let assignments = abi::build_outgoing_arg_assignments_for_target(emitter.target, arg_types, 0); let overflow_bytes = abi::materialize_outgoing_args(emitter, &assignments); save_concat_offset_before_nested_call(emitter); + let call_pad_bytes = if emitter.target.arch == Arch::AArch64 { + // `save_concat_offset_before_nested_call` already pushes one 16-byte slot on + // AArch64, which is the exact gap expected above spilled arguments. + 0 + } else { + abi::outgoing_call_stack_pad_bytes(emitter.target, overflow_bytes) + }; + abi::emit_reserve_temporary_stack(emitter, call_pad_bytes); abi::emit_call_reg(emitter, call_reg); + abi::emit_release_temporary_stack(emitter, call_pad_bytes); restore_concat_offset_after_nested_call(emitter, &sig.return_type); abi::emit_release_temporary_stack(emitter, overflow_bytes); } @@ -2115,8 +2122,8 @@ fn emit_loaded_assoc_variadic_array_arg( 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); + let capacity_reg = abi::runtime_helper_int_arg_reg(emitter, 0); + let tag_reg = abi::runtime_helper_int_arg_reg(emitter, 1); abi::emit_load_int_immediate(emitter, capacity_reg, 16); abi::emit_load_int_immediate( @@ -2512,6 +2519,69 @@ mod tests { use super::*; use crate::codegen::platform::{Platform, Target}; + /// Verifies nested Windows callable targets receive the mandatory MS x64 shadow space. + #[test] + fn windows_invoker_target_call_reserves_shadow_space() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + let sig = FunctionSig { + params: Vec::new(), + param_type_exprs: Vec::new(), + param_attributes: Vec::new(), + defaults: Vec::new(), + return_type: PhpType::Int, + declared_return: false, + by_ref_return: false, + ref_params: Vec::new(), + declared_params: Vec::new(), + variadic: None, + deprecation: None, + }; + + call_target_with_pushed_args("r12", &[], &sig, &mut emitter); + + let output = emitter.output(); + let reserve = output.find(" sub rsp, 32\n").expect("shadow-space reserve"); + let call = output.find(" call r12\n").expect("target call"); + let release = output.rfind(" add rsp, 32\n").expect("shadow-space release"); + assert!(reserve < call && call < release); + } + + /// Verifies the saved concat slot itself provides ARM64's spilled-argument call gap. + #[test] + fn arm64_invoker_target_call_does_not_double_pad_spilled_args() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::AArch64)); + let sig = FunctionSig { + params: Vec::new(), + param_type_exprs: Vec::new(), + param_attributes: Vec::new(), + defaults: Vec::new(), + return_type: PhpType::Int, + declared_return: false, + by_ref_return: false, + ref_params: Vec::new(), + declared_params: Vec::new(), + variadic: None, + deprecation: None, + }; + let arg_types = vec![PhpType::Int; 9]; + + call_target_with_pushed_args("x20", &arg_types, &sig, &mut emitter); + + let output = emitter.output(); + let save_concat = output + .find("str x10, [sp, #-16]!") + .expect("concat-offset save"); + let call = output[save_concat..] + .find("blr x20") + .map(|offset| save_concat + offset) + .expect("nested callable target call"); + let call_setup = &output[save_concat..call]; + assert!( + !call_setup.contains("sub sp, sp, #16"), + "the concat save already supplies ARM64's 16-byte call gap:\n{call_setup}" + ); + } + /// Verifies expanded ARM64 invoker boundaries materialize far frame-slot addresses. #[test] fn arm64_invoker_boundary_uses_large_offset_frame_helpers() { diff --git a/src/codegen_support/abi/bootstrap.rs b/src/codegen_support/abi/bootstrap.rs index b8b7c6fc46..675d9d9b55 100644 --- a/src/codegen_support/abi/bootstrap.rs +++ b/src/codegen_support/abi/bootstrap.rs @@ -8,7 +8,7 @@ //! Key details: //! - Register choices must match the platform entry convention before normal PHP frame setup begins. -use crate::codegen_support::{emit::Emitter, platform::Arch}; +use crate::codegen_support::{emit::Emitter, platform::{Arch, Platform}}; use super::{ emit_load_int_immediate, emit_store_reg_to_symbol, process_argc_reg, process_argv_reg, @@ -16,7 +16,24 @@ use super::{ }; /// Store OS-provided argc and argv into global symbols. +/// +/// On macOS-AArch64, Linux-AArch64, and Linux-x86_64 the process entry +/// convention places argc/argv in the arch's first/second integer argument +/// registers (`x0`/`x1`, `rdi`/`rsi`), which are stored directly into +/// `_global_argc`/`_global_argv` byte-for-byte as before. +/// +/// On Windows-x86_64 the MinGW CRT `main` wrapper receives `argc` as a 32-bit +/// `int` in `ecx` whose upper 32 bits are not defined by MSx64; spilling full +/// `rcx`/`rdx` can leave a garbage `_global_argc` that makes `__rt_build_argv` +/// write to NULL. Instead this path emits a single `call __rt_sys_init_argv`, +/// a Unicode Win32 shim that parses `GetCommandLineW` through +/// `CommandLineToArgvW` and stores strict UTF-8 arguments in the globals. The AArch64 and +/// Linux-x86_64 paths remain byte-identical to the original direct stores. pub fn emit_store_process_args_to_globals(emitter: &mut Emitter) { + if (emitter.target.platform, emitter.target.arch) == (Platform::Windows, Arch::X86_64) { + emitter.instruction("call __rt_sys_init_argv"); // populate globals from native Unicode Windows argv + return; + } 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); } @@ -46,29 +63,40 @@ pub fn emit_copy_frame_pointer(emitter: &mut Emitter, dest: &str) { /// # 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`). +/// - **Windows x86_64**: loads `code` into `edi` and calls the `__rt_sys_exit` shim +/// (`ExitProcess`), which reads `rdi`. Terminating here — identical to an explicit +/// `exit(code)` — instead of returning through the MinGW CRT is deliberate: the CRT's +/// `exit` reaches the same `rdi`-consuming shim, so a return path that left `rdi` +/// holding leftover data would exit with a garbage code. /// - **macOS x86_64**: panics — not yet implemented. /// -/// This routine never returns to the calling code. The syscall consumes the current execution context. +/// This routine never returns to the calling code. The exit 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) => { + (Platform::MacOS, Arch::AArch64) | (Platform::Linux, Arch::AArch64) => { emitter.instruction("bl __rt_ob_flush_all"); // drain still-active output buffers to stdout before terminating 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) => { + (Platform::Linux, Arch::X86_64) => { emitter.instruction("and rsp, -16"); // realign the stack for the flush call (this path never returns) emitter.instruction("call __rt_ob_flush_all"); // drain still-active output buffers to stdout before terminating 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) => { + (Platform::MacOS, Arch::X86_64) => { panic!("process exit emission is not implemented yet for target macos-x86_64"); } - (super::super::platform::Platform::Windows, _) => { - panic!("Windows target is not yet supported (see issue #379)"); + (Platform::Windows, Arch::X86_64) => { + emitter.instruction(&format!("mov ebx, {}", code)); // preserve the requested exit code across the output-buffer flush + emitter.instruction("and rsp, -16"); // realign the stack for runtime calls on this terminal path + emitter.instruction("call __rt_ob_flush_all"); // drain still-active output buffers before terminating + emitter.instruction("mov rdi, rbx"); // load the runtime shim's SysV-style integer argument register + emitter.instruction("call __rt_sys_exit"); // terminate via the Win32 ExitProcess shim, which reads rdi (never returns) + } + (Platform::Windows, Arch::AArch64) => { + panic!("Windows ARM64 target is not yet supported (see issue #379)"); } } } @@ -85,19 +113,21 @@ pub fn emit_exit(emitter: &mut Emitter, code: u32) { /// 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`). +/// - **Windows x86_64**: moves `eax` into `edi` and calls the `__rt_sys_exit` shim +/// (`ExitProcess`), which reads `rdi` — terminating directly rather than returning +/// through the MinGW CRT, for the same `rdi`-consuming reason as `emit_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) => { + (Platform::MacOS, Arch::AArch64) | (Platform::Linux, Arch::AArch64) => { emitter.instruction("mov x19, x0"); // stash the exit code in a callee-saved register (this path never returns) emitter.instruction("bl __rt_ob_flush_all"); // drain still-active output buffers to stdout before terminating emitter.instruction("mov x0, x19"); // restore the exit code into the syscall argument register emitter.syscall(1); } - (super::super::platform::Platform::Linux, Arch::X86_64) => { + (Platform::Linux, Arch::X86_64) => { emitter.instruction("mov rbx, rax"); // stash the exit code in a callee-saved register (this path never returns) emitter.instruction("and rsp, -16"); // realign the stack for the flush call (this path never returns) emitter.instruction("call __rt_ob_flush_all"); // drain still-active output buffers to stdout before terminating @@ -105,11 +135,18 @@ pub fn emit_exit_with_result_reg(emitter: &mut Emitter) { 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) => { + (Platform::MacOS, Arch::X86_64) => { panic!("process exit emission is not implemented yet for target macos-x86_64"); } - (super::super::platform::Platform::Windows, _) => { - panic!("Windows target is not yet supported (see issue #379)"); + (Platform::Windows, Arch::X86_64) => { + emitter.instruction("mov rbx, rax"); // preserve the bridge return value across the output-buffer flush + emitter.instruction("and rsp, -16"); // realign the stack for runtime calls on this terminal path + emitter.instruction("call __rt_ob_flush_all"); // drain still-active output buffers before terminating + emitter.instruction("mov rdi, rbx"); // load the runtime shim's SysV-style integer argument register + emitter.instruction("call __rt_sys_exit"); // terminate via the Win32 ExitProcess shim, which reads rdi (never returns) + } + (Platform::Windows, Arch::AArch64) => { + panic!("Windows ARM64 target is not yet supported (see issue #379)"); } } } diff --git a/src/codegen_support/abi/callbacks.rs b/src/codegen_support/abi/callbacks.rs new file mode 100644 index 0000000000..ca37a28c0d --- /dev/null +++ b/src/codegen_support/abi/callbacks.rs @@ -0,0 +1,70 @@ +//! Purpose: +//! Adapts native C callback entry points to the hand-written runtime helper ABI. +//! Keeps Windows MSx64 register and stack placement out of eval wrapper emitters. +//! +//! Called from: +//! - C-visible eval and reflection callback label emitters. +//! +//! Key details: +//! - Hand-written x86_64 callback bodies consume the SysV-shaped integer registers. +//! - Windows overflow offsets include the mandatory 32-byte shadow space. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::{Arch, Platform, Target}; + +/// Returns the private SysV-shaped entry label used by generated assembly calls +/// that must bypass the native C callback adapter. +pub fn c_callback_internal_symbol(target: Target, name: &str) -> String { + target.extern_symbol(&format!("{}__internal", name)) +} + +/// Emits a native C-visible callback label followed by its target adapter and +/// a private label for calls already using the hand-written helper ABI. +pub fn emit_c_callback_entry(emitter: &mut Emitter, name: &str) { + let symbol = emitter.target.extern_symbol(name); + emitter.label_global(&symbol); + emit_c_callback_sysv_register_adapter(emitter); + let internal_symbol = c_callback_internal_symbol(emitter.target, name); + match emitter.target.arch { + Arch::AArch64 => emitter.instruction(&format!("b {}", internal_symbol)), // enter the adapter-free callback implementation + Arch::X86_64 => emitter.instruction(&format!("jmp {}", internal_symbol)), // enter the adapter-free callback implementation + } + emitter.raw(&format!(".globl {}", internal_symbol)); + emitter.raw(&format!("{}:", internal_symbol)); +} + +/// Normalizes an incoming Windows x86_64 C callback's first six integer arguments +/// into the register layout consumed by the hand-written SysV-shaped helper body. +fn emit_c_callback_sysv_register_adapter(emitter: &mut Emitter) { + if (emitter.target.platform, emitter.target.arch) != (Platform::Windows, Arch::X86_64) { + return; + } + + emit_windows_c_abi_registers_for_runtime_helper(emitter); + emitter.instruction("mov r8, QWORD PTR [rsp + 40]"); // normalize callback argument 5 from the Windows caller stack + emitter.instruction("mov r9, QWORD PTR [rsp + 48]"); // normalize callback argument 6 from the Windows caller stack +} + +/// Normalizes four materialized MSx64 integer arguments for a hand-written +/// x86_64 runtime helper that consumes the repository's SysV-shaped ABI. +pub fn emit_windows_c_abi_registers_for_runtime_helper(emitter: &mut Emitter) { + if (emitter.target.platform, emitter.target.arch) != (Platform::Windows, Arch::X86_64) { + return; + } + + emitter.instruction("mov rdi, rcx"); // normalize callback argument 1 from MSx64 to the runtime helper ABI + emitter.instruction("mov rsi, rdx"); // normalize callback argument 2 from MSx64 to the runtime helper ABI + emitter.instruction("mov rdx, r8"); // normalize callback argument 3 from MSx64 to the runtime helper ABI + emitter.instruction("mov rcx, r9"); // normalize callback argument 4 from MSx64 to the runtime helper ABI +} + +/// Returns the frame-pointer offset for a zero-based integer callback argument +/// that follows the six registers consumed by a SysV-shaped x86_64 helper body. +pub fn c_callback_stack_arg_offset(target: Target, argument_index: usize) -> usize { + assert!(argument_index >= 6, "callback stack arguments start at index 6"); + match (target.platform, target.arch) { + (Platform::Windows, Arch::X86_64) => 48 + (argument_index - 4) * 8, + (_, Arch::X86_64) => 16 + (argument_index - 6) * 8, + (_, Arch::AArch64) => panic!("x86_64 callback stack offsets are not valid on AArch64"), + } +} diff --git a/src/codegen_support/abi/calls/mod.rs b/src/codegen_support/abi/calls/mod.rs index 60e968a233..3d30025aa3 100644 --- a/src/codegen_support/abi/calls/mod.rs +++ b/src/codegen_support/abi/calls/mod.rs @@ -16,8 +16,8 @@ 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, - outgoing_call_stack_pad_bytes, + build_c_abi_outgoing_arg_assignments_for_target, build_outgoing_arg_assignments_for_target, + compact_windows_c_abi_stack_args, materialize_outgoing_args, outgoing_call_stack_pad_bytes, }; pub use stack::{ emit_load_temporary_stack_slot, emit_pop_float_reg, emit_pop_reg, emit_pop_reg_pair, diff --git a/src/codegen_support/abi/calls/outgoing.rs b/src/codegen_support/abi/calls/outgoing.rs index 6fb917ad41..cbdf7044a0 100644 --- a/src/codegen_support/abi/calls/outgoing.rs +++ b/src/codegen_support/abi/calls/outgoing.rs @@ -10,7 +10,7 @@ use crate::codegen_support::{ emit::Emitter, - platform::{Arch, Target}, + platform::{Arch, Platform, Target}, }; use crate::types::PhpType; @@ -82,6 +82,78 @@ pub fn build_outgoing_arg_assignments_for_target( assignments } +/// Plans outgoing arguments for the native C ABI rather than elephc's PHP ABI. +/// +/// Windows x86_64 assigns the first four C arguments by positional slot: an +/// integer in slot 0 uses `rcx`, while a float in slot 1 uses `xmm1`. The PHP +/// ABI intentionally tracks integer and floating-point registers separately, +/// so foreign calls must use this planner to avoid collapsing mixed signatures +/// onto `rcx`/`xmm0`. Other targets share the regular assignment model. +pub fn build_c_abi_outgoing_arg_assignments_for_target( + target: Target, + arg_types: &[PhpType], +) -> Vec { + if (target.platform, target.arch) != (Platform::Windows, Arch::X86_64) { + return build_outgoing_arg_assignments_for_target(target, arg_types, 0); + } + + let mut assignments = Vec::with_capacity(arg_types.len()); + let mut slot = 0usize; + let mut stack_only = false; + for ty in arg_types { + let slot_count = ty.register_count(); + if !stack_only && slot + slot_count <= 4 { + assignments.push(OutgoingArgAssignment { + ty: ty.clone(), + start_reg: slot, + is_float: ty.is_float_reg(), + }); + slot += slot_count; + } else { + assignments.push(OutgoingArgAssignment { + ty: ty.clone(), + start_reg: STACK_ARG_SENTINEL, + is_float: ty.is_float_reg(), + }); + stack_only = true; + } + } + assignments +} + +/// Compacts generic 16-byte overflow temporaries into native 8-byte MSx64 C slots. +/// +/// `materialize_outgoing_args` deliberately leaves one 16-byte slot per PHP +/// argument. Native Windows calls instead require consecutive eight-byte stack +/// slots after shadow space. This post-pass preserves the reserved byte count +/// while moving each overflow payload down to its C ABI position. It is a no-op +/// on all other targets. +pub fn compact_windows_c_abi_stack_args( + emitter: &mut Emitter, + assignments: &[OutgoingArgAssignment], +) { + if (emitter.target.platform, emitter.target.arch) != (Platform::Windows, Arch::X86_64) { + return; + } + let mut source_offset = 0usize; + let mut destination_offset = 0usize; + for assignment in assignments + .iter() + .filter(|assignment| !assignment.in_register()) + { + if source_offset != destination_offset { + emit_copy_stack_arg_slot( + emitter, + &assignment.ty, + source_offset, + destination_offset, + ); + } + source_offset += arg_slot_size(&assignment.ty); + destination_offset += assignment.ty.register_count() * 8; + } +} + /// Returns the stack slot byte size for `ty`. /// /// All argument types use a 16-byte slot on the temporary stack (8-byte pointer + 8-byte @@ -261,8 +333,9 @@ pub fn materialize_outgoing_args( /// x86_64 already gets the corresponding gap from `call` pushing the return address and the /// callee saving `rbp`. pub fn outgoing_call_stack_pad_bytes(target: Target, overflow_bytes: usize) -> usize { - match target.arch { - Arch::AArch64 if overflow_bytes > 0 => 16, + match (target.platform, target.arch) { + (Platform::Windows, Arch::X86_64) => 32, + (_, Arch::AArch64) if overflow_bytes > 0 => 16, _ => 0, } } diff --git a/src/codegen_support/abi/frame.rs b/src/codegen_support/abi/frame.rs index a7a8a826f7..5a539a2021 100644 --- a/src/codegen_support/abi/frame.rs +++ b/src/codegen_support/abi/frame.rs @@ -8,7 +8,10 @@ //! Key details: //! - Frame offsets and stack alignment are shared contracts with local collection and call materialization. -use crate::codegen_support::{emit::Emitter, platform::Arch}; +use crate::codegen_support::{ + emit::Emitter, + platform::{Arch, Platform}, +}; #[cfg(test)] use crate::types::PhpType; @@ -47,6 +50,22 @@ pub fn emit_frame_prologue(emitter: &mut Emitter, frame_size: usize) { } Arch::X86_64 => { let local_bytes = frame_size.saturating_sub(16); + if emitter.target.platform == Platform::Windows { + let required_bytes = local_bytes + 8 + 16 * 1024; + // -- fail before a Fiber frame can consume its inaccessible guard -- + emitter.instruction("mov r11, QWORD PTR [rip + _fiber_current]"); // load the active Fiber object, if execution is on a Fiber stack + emitter.instruction("test r11, r11"); // main-stack execution has no Fiber guard to enforce + emitter.instruction("jz 8f"); // skip the guard check outside a running Fiber + emitter.instruction("mov r10, QWORD PTR [r11 + 16]"); // load this Fiber's mapping base (the guard's first byte) + emitter.instruction("add r10, 16384"); // advance to the first usable byte above the 16 KiB guard + let prospective_sp = format!("lea rax, [rsp - {}]", required_bytes); + emitter.instruction(&prospective_sp); // include this frame and emergency-exit reserve in the prospective SP + emitter.instruction("cmp rax, r10"); // would the prospective stack consume the guard or emergency reserve? + emitter.instruction("jae 8f"); // enough usable Fiber stack remains for the complete frame + emitter.instruction("call __rt_win_fiber_stack_overflow_abort"); // terminate deterministically before touching PAGE_NOACCESS + emitter.instruction("ud2"); // the emergency abort never returns; trap if the OS contract is violated + emitter.label("8"); + } 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 { diff --git a/src/codegen_support/abi/mod.rs b/src/codegen_support/abi/mod.rs index 8e9da261f5..e4e323fc9d 100644 --- a/src/codegen_support/abi/mod.rs +++ b/src/codegen_support/abi/mod.rs @@ -9,6 +9,7 @@ //! - Shared lowering should go through this module instead of hardcoding platform registers or stack rules. mod bootstrap; +mod callbacks; mod calls; mod frame; mod registers; @@ -23,8 +24,13 @@ pub use bootstrap::{ emit_enable_heap_debug_flag, emit_exit, emit_exit_with_result_reg, emit_store_process_args_to_globals, }; +pub use callbacks::{ + c_callback_internal_symbol, c_callback_stack_arg_offset, emit_c_callback_entry, + emit_windows_c_abi_registers_for_runtime_helper, +}; pub use calls::{ - build_outgoing_arg_assignments_for_target, emit_call_label, emit_call_reg, + build_c_abi_outgoing_arg_assignments_for_target, build_outgoing_arg_assignments_for_target, + compact_windows_c_abi_stack_args, emit_call_label, emit_call_reg, 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_incoming_param, @@ -40,9 +46,9 @@ pub use frame::{ #[cfg(test)] pub use frame::{emit_preserve_return_value, emit_restore_return_value}; pub(crate) use registers::{ - float_arg_reg_name, float_result_reg, float_spill_scratch_reg, int_arg_reg_name, - int_result_reg, secondary_scratch_reg, string_result_regs, symbol_scratch_reg, - tertiary_scratch_reg, + float_arg_reg_name, float_result_reg, float_spill_scratch_reg, + int_arg_reg_name, int_result_reg, runtime_helper_int_arg_reg, secondary_scratch_reg, + string_result_regs, symbol_scratch_reg, tertiary_scratch_reg, }; pub use registers::{ nested_call_reg, process_argc_reg, process_argv_reg, temp_int_reg, IncomingArgCursor, diff --git a/src/codegen_support/abi/registers.rs b/src/codegen_support/abi/registers.rs index fe1f3f7558..6c52b6fb27 100644 --- a/src/codegen_support/abi/registers.rs +++ b/src/codegen_support/abi/registers.rs @@ -21,38 +21,88 @@ const CALLER_STACK_START_OFFSET: usize = 32; 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). +/// AArch64: 8 (x0–x7). Linux x86_64: 6 (rdi, rsi, rdx, rcx, r8, r9). Windows x86_64: 4 (rcx, rdx, 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, + match (target.platform, target.arch) { + (Platform::MacOS, Arch::AArch64) => MAX_INT_ARG_REGS, + (Platform::Linux, Arch::AArch64) => MAX_INT_ARG_REGS, + (Platform::Linux, Arch::X86_64) => 6, + (Platform::Windows, Arch::X86_64) => 4, + (Platform::MacOS, Arch::X86_64) => 6, + (Platform::Windows, Arch::AArch64) => { + panic!("Windows ARM64 target is not yet supported (see issue #379)") + } } } /// 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). +/// AArch64: 8 (d0–d7). Linux x86_64: 8 (xmm0–xmm7). Windows x86_64: 4 (xmm0–xmm3). 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, + match (target.platform, target.arch) { + (Platform::MacOS, Arch::AArch64) => MAX_FLOAT_ARG_REGS, + (Platform::Linux, Arch::AArch64) => MAX_FLOAT_ARG_REGS, + (Platform::Linux, Arch::X86_64) => MAX_FLOAT_ARG_REGS, + (Platform::Windows, Arch::X86_64) => 4, + (Platform::MacOS, Arch::X86_64) => MAX_FLOAT_ARG_REGS, + (Platform::Windows, Arch::AArch64) => { + panic!("Windows ARM64 target is not yet supported (see issue #379)") + } } } /// 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. +/// Linux x86_64 reaches the first stack arg at rbp+16 after call pushes the return address. +/// Windows x86_64 reaches the first stack arg at rbp+48 after the callee saves rbp +/// (32-byte shadow space + return address + saved frame pointer). pub(crate) fn caller_stack_start_offset(target: Target) -> usize { - match target.arch { - Arch::AArch64 => CALLER_STACK_START_OFFSET, - Arch::X86_64 => 16, + match (target.platform, target.arch) { + (Platform::MacOS, Arch::AArch64) => CALLER_STACK_START_OFFSET, + (Platform::Linux, Arch::AArch64) => CALLER_STACK_START_OFFSET, + (Platform::Linux, Arch::X86_64) => 16, + (Platform::Windows, Arch::X86_64) => 48, + (Platform::MacOS, Arch::X86_64) => 16, + (Platform::Windows, Arch::AArch64) => { + panic!("Windows ARM64 target is not yet supported (see issue #379)") + } } } /// 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 { + match (target.platform, target.arch) { + (Platform::MacOS, Arch::AArch64) => ["x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7"][idx], + (Platform::Linux, Arch::AArch64) => ["x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7"][idx], + (Platform::Linux, Arch::X86_64) => ["rdi", "rsi", "rdx", "rcx", "r8", "r9"][idx], + (Platform::Windows, Arch::X86_64) => ["rcx", "rdx", "r8", "r9"][idx], + (Platform::MacOS, Arch::X86_64) => ["rdi", "rsi", "rdx", "rcx", "r8", "r9"][idx], + (Platform::Windows, Arch::AArch64) => { + panic!("Windows ARM64 target is not yet supported (see issue #379)") + } + } +} + +/// Returns the register used to pass the `idx`-th integer argument to a hand-written, +/// System V AMD64-only `__rt_*` runtime helper (defined in `codegen_support::runtime::**` +/// and reading its args from `rdi`/`rsi`/`rdx`/`rcx`/`r8`/`r9` on every target). +/// +/// Use THIS function — never `int_arg_reg_name` — when staging arguments for such a +/// hand-written SysV helper: on `windows-x86_64`, `int_arg_reg_name` yields the MSx64 +/// sequence (`rcx`/`rdx`/`r8`/`r9`), which would stage the argument in the wrong +/// register and poison the call. Use `int_arg_reg_name` instead when the callee is a +/// callable EMITTED by the codegen itself (descriptor invoker, user PHP function, or any +/// callee that re-derives its own argument registers via `int_arg_reg_name` on the same +/// target) — those callees expect MSx64 registers on `windows-x86_64` too, so both sides +/// agree. AArch64 has a single calling convention, and Linux/macOS x86_64 already use the +/// SysV register order, so this function returns byte-identical values to +/// `int_arg_reg_name` on every non-Windows target — the register choice only diverges on +/// `windows-x86_64`. +pub(crate) fn runtime_helper_int_arg_reg(emitter: &Emitter, idx: usize) -> &'static str { + match emitter.target.arch { + // AAPCS64 passes the first 8 integer args in x0-x7; x86_64 SysV has only 6 + // integer arg registers (rdi/rsi/rdx/rcx/r8/r9), so the tables differ in length. Arch::AArch64 => ["x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7"][idx], Arch::X86_64 => ["rdi", "rsi", "rdx", "rcx", "r8", "r9"][idx], } @@ -61,9 +111,15 @@ pub(crate) fn int_arg_reg_name(target: Target, idx: usize) -> &'static str { /// 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], + match (target.platform, target.arch) { + (Platform::MacOS, Arch::AArch64) => ["d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7"][idx], + (Platform::Linux, Arch::AArch64) => ["d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7"][idx], + (Platform::Linux, Arch::X86_64) => ["xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7"][idx], + (Platform::Windows, Arch::X86_64) => ["xmm0", "xmm1", "xmm2", "xmm3"][idx], + (Platform::MacOS, Arch::X86_64) => ["xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7"][idx], + (Platform::Windows, Arch::AArch64) => { + panic!("Windows ARM64 target is not yet supported (see issue #379)") + } } } diff --git a/src/codegen_support/abi/symbols.rs b/src/codegen_support/abi/symbols.rs index e10190c8d2..923c129fcf 100644 --- a/src/codegen_support/abi/symbols.rs +++ b/src/codegen_support/abi/symbols.rs @@ -9,7 +9,10 @@ //! - Symbol relocations differ by platform and refcounted stores must preserve ownership cleanup. use crate::codegen_support::NULL_SENTINEL; -use crate::codegen_support::{emit::Emitter, platform::Arch}; +use crate::codegen_support::{ + emit::Emitter, + platform::{Arch, Platform}, +}; use crate::types::PhpType; use super::calls::emit_call_label; @@ -125,7 +128,18 @@ pub fn emit_symbol_address(emitter: &mut Emitter, dest: &str, symbol: &str) { /// 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. +/// +/// Windows x86_64 is the one exception: PE/COFF has no ELF/Mach-O-style +/// GOTPCREL relocation, and MinGW's assembler treats `@GOTPCREL` as a literal +/// name suffix rather than a relocation, so it fails to link. These "extern" +/// symbols resolve against a statically-linked `.a` archive built into the +/// same image rather than a DLL, so no `__imp_` indirection is needed either +/// — a direct RIP-relative LEA is the correct, GOT-free form there. pub fn emit_extern_symbol_address(emitter: &mut Emitter, dest: &str, symbol: &str) { + if (emitter.target.platform, emitter.target.arch) == (Platform::Windows, Arch::X86_64) { + emitter.instruction(&format!("lea {}, [rip + {}]", dest, symbol)); // GOT-free RIP-relative LEA (PE/COFF static-archive extern symbol) + return; + } match emitter.target.arch { Arch::AArch64 => { emitter.adrp_got(dest, symbol); // load the GOT page that points at the requested extern symbol diff --git a/src/codegen_support/abi/tests.rs b/src/codegen_support/abi/tests.rs index 7fb5057d5d..d956a965cd 100644 --- a/src/codegen_support/abi/tests.rs +++ b/src/codegen_support/abi/tests.rs @@ -33,5 +33,6 @@ fn test_emitter_x86() -> Emitter { mod basics; mod arguments; +mod callbacks; mod symbols; mod linux_x86_64; diff --git a/src/codegen_support/abi/tests/arguments.rs b/src/codegen_support/abi/tests/arguments.rs index 94eff2e5e6..464f57d88f 100644 --- a/src/codegen_support/abi/tests/arguments.rs +++ b/src/codegen_support/abi/tests/arguments.rs @@ -169,6 +169,132 @@ fn test_materialize_outgoing_float_overflow_uses_volatile_scratch() { assert!(!out.contains("d15")); } +/// Verifies Windows calls always reserve the mandatory four-slot caller home area. +#[test] +fn test_windows_outgoing_call_stack_pad_reserves_shadow_space_without_overflow() { + let target = Target::new(Platform::Windows, Arch::X86_64); + assert_eq!(outgoing_call_stack_pad_bytes(target, 0), 32); +} + +/// Verifies Windows overflow arguments remain positioned after the fixed shadow area. +#[test] +fn test_windows_outgoing_call_stack_pad_precedes_overflow_slots() { + let target = Target::new(Platform::Windows, Arch::X86_64); + assert_eq!(outgoing_call_stack_pad_bytes(target, 32), 32); +} + +/// Verifies that native Windows C arguments share four positional register slots. +#[test] +fn test_windows_c_abi_mixed_arguments_use_positional_register_slots() { + let assignments = build_c_abi_outgoing_arg_assignments_for_target( + Target::new(Platform::Windows, Arch::X86_64), + &[ + PhpType::Int, + PhpType::Float, + PhpType::Int, + PhpType::Float, + PhpType::Int, + ], + ); + + assert_eq!(assignments[0].start_reg, 0); + assert_eq!(assignments[1].start_reg, 1); + assert_eq!(assignments[2].start_reg, 2); + assert_eq!(assignments[3].start_reg, 3); + assert_eq!(assignments[4].start_reg, crate::codegen_support::abi::registers::STACK_ARG_SENTINEL); + assert!(!assignments[0].is_float); + assert!(assignments[1].is_float); +} + +/// Verifies that non-Windows native C calls retain the regular target ABI plan. +#[test] +fn test_non_windows_c_abi_plan_matches_regular_plan() { + let target = Target::new(Platform::Linux, Arch::X86_64); + let types = [PhpType::Int, PhpType::Float, PhpType::Int]; + + assert_eq!( + build_c_abi_outgoing_arg_assignments_for_target(target, &types), + build_outgoing_arg_assignments_for_target(target, &types, 0) + ); +} + +/// Verifies that MSx64 overflow slots are compacted from PHP's 16-byte staging layout. +#[test] +fn test_windows_c_abi_compacts_overflow_to_eight_byte_slots() { + let target = Target::new(Platform::Windows, Arch::X86_64); + let mut emitter = Emitter::new(target); + let assignments = build_c_abi_outgoing_arg_assignments_for_target( + target, + &[ + PhpType::Int, + PhpType::Int, + PhpType::Int, + PhpType::Int, + PhpType::Int, + PhpType::Float, + PhpType::Int, + ], + ); + + compact_windows_c_abi_stack_args(&mut emitter, &assignments); + let out = emitter.output(); + + assert!(out.contains("movsd xmm15, QWORD PTR [rsp + 16]")); + assert!(out.contains("movsd QWORD PTR [rsp + 8], xmm15")); + assert!(out.contains("mov r10, QWORD PTR [rsp + 32]")); + assert!(out.contains("mov QWORD PTR [rsp + 16], r10")); +} + +/// Verifies elephc's Windows callee reads owned overflow shapes after the full +/// MSx64 shadow/return-address/saved-rbp prefix and advances in 16-byte slots. +#[test] +fn test_windows_incoming_owned_overflow_shapes_use_internal_stack_slots() { + let target = Target::new(Platform::Windows, Arch::X86_64); + let mut emitter = Emitter::new(target); + let mut cursor = IncomingArgCursor::for_target(target, 0); + for (index, name) in ["a", "b", "c", "d"].iter().enumerate() { + emit_store_incoming_param( + &mut emitter, + name, + &PhpType::Int, + (index + 1) * 16, + false, + &mut cursor, + ); + } + emit_store_incoming_param( + &mut emitter, + "label", + &PhpType::Str, + 80, + false, + &mut cursor, + ); + emit_store_incoming_param( + &mut emitter, + "object", + &PhpType::Object("Payload".to_string()), + 96, + false, + &mut cursor, + ); + emit_store_incoming_param( + &mut emitter, + "mixed", + &PhpType::Mixed, + 112, + false, + &mut cursor, + ); + + let out = emitter.output(); + assert!(out.contains("param $label from caller stack +48")); + assert!(out.contains("mov r10, QWORD PTR [rbp + 48]"), "{out}"); + assert!(out.contains("mov rcx, QWORD PTR [rbp + 56]"), "{out}"); + assert!(out.contains("param $object from caller stack +64")); + assert!(out.contains("param $mixed from caller stack +80")); +} + /// Tests that `emit_store_local_slot_to_symbol` handles string slots with large /// offsets (>4095) by emitting the necessary adrp/add page calculations and /// decomposed sub instructions to reach the slot, then stores both x10 and x11 diff --git a/src/codegen_support/abi/tests/basics.rs b/src/codegen_support/abi/tests/basics.rs index 30236fd106..21e07a5689 100644 --- a/src/codegen_support/abi/tests/basics.rs +++ b/src/codegen_support/abi/tests/basics.rs @@ -44,6 +44,32 @@ fn test_emit_frame_prologue_rejects_undersized_frame() { emit_frame_prologue(&mut emitter, 8); } +/// Verifies Windows function prologues reserve emergency stack space while a +/// Fiber is active and abort before writing into that Fiber's guard page. +#[test] +fn test_windows_frame_prologue_checks_active_fiber_guard() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_frame_prologue(&mut emitter, 256); + let asm = emitter.output(); + + assert!(asm.contains("mov r11, QWORD PTR [rip + _fiber_current]")); + assert!(asm.contains("mov r10, QWORD PTR [r11 + 16]")); + assert!(asm.contains("add r10, 16384")); + assert!(asm.contains("lea rax, [rsp - 16632]")); + assert!(asm.contains("call __rt_win_fiber_stack_overflow_abort")); + assert!(asm.find("call __rt_win_fiber_stack_overflow_abort") < asm.find("push rbp")); +} + +/// Verifies Linux x86_64 prologues remain free of the Windows Fiber guard +/// enforcement sequence. +#[test] +fn test_linux_frame_prologue_does_not_emit_windows_fiber_guard_check() { + let mut emitter = test_emitter_x86(); + emit_frame_prologue(&mut emitter, 256); + + assert!(!emitter.output().contains("__rt_win_fiber_stack_overflow_abort")); +} + /// Tests that string return values (pointer in x1, length in x2) are preserved /// across function boundaries by storing them to the caller's stack frame at negative /// offsets and restoring them after the call. Uses offset 32 for both stores. diff --git a/src/codegen_support/abi/tests/callbacks.rs b/src/codegen_support/abi/tests/callbacks.rs new file mode 100644 index 0000000000..6842a53462 --- /dev/null +++ b/src/codegen_support/abi/tests/callbacks.rs @@ -0,0 +1,75 @@ +//! Purpose: +//! Verifies target-aware native C callback entry adaptation. +//! Pins Windows MSx64 register normalization and overflow stack offsets. +//! +//! Called from: +//! - `crate::codegen_support::abi::tests` through Rust's test harness. +//! +//! Key details: +//! - Unix callback entries must remain byte-free of the Windows adapter. + +use super::*; + +/// Verifies Windows callback entries normalize all SysV-shaped integer inputs +/// and expose a private adapter-free label for generated assembly calls. +#[test] +fn test_windows_c_callback_entry_adapts_msx64_registers() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + + emit_c_callback_entry(&mut emitter, "__elephc_eval_value_string_bytes"); + let output = emitter.output(); + + assert!(output.contains("mov rdi, rcx")); + assert!(output.contains("mov rsi, rdx")); + assert!(output.contains("mov rdx, r8")); + assert!(output.contains("mov rcx, r9")); + assert!(output.contains("mov r8, QWORD PTR [rsp + 40]")); + assert!(output.contains("mov r9, QWORD PTR [rsp + 48]")); + assert!(output.contains("__elephc_eval_value_string_bytes__internal:")); +} + +/// Verifies Unix x86_64 callback entries preserve their native SysV registers. +#[test] +fn test_linux_c_callback_entry_needs_no_register_adapter() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + + emit_c_callback_entry(&mut emitter, "__elephc_eval_value_string_bytes"); + let output = emitter.output(); + + assert!(!output.contains("mov rdi, rcx")); + assert!(output.contains("__elephc_eval_value_string_bytes__internal:")); +} + +/// Verifies Mach-O dead-strip localization does not rewrite a cross-object +/// callback alias into an assembler-local symbol that `.globl` cannot export. +#[test] +fn test_macos_c_callback_internal_alias_is_not_localized() { + let mut emitter = Emitter::new(Target::new(Platform::MacOS, Arch::AArch64)); + emitter.dead_strip = true; + + emit_c_callback_entry(&mut emitter, "__elephc_eval_value_string_bytes"); + + assert!(emitter.take_internal_labels().is_empty()); + let output = emitter.output(); + assert!(output.contains(".globl ___elephc_eval_value_string_bytes__internal")); + assert!(!output.contains(".globl L___elephc_eval_value_string_bytes__internal")); +} + +/// Verifies high-arity Windows callbacks skip shadow space and the two stack +/// arguments promoted into the SysV-shaped r8/r9 registers. +#[test] +fn test_windows_c_callback_stack_offsets_cover_high_arity_hooks() { + let target = Target::new(Platform::Windows, Arch::X86_64); + + assert_eq!(c_callback_stack_arg_offset(target, 6), 64); + assert_eq!(c_callback_stack_arg_offset(target, 16), 144); +} + +/// Verifies SysV callback overflow arguments retain their original frame offsets. +#[test] +fn test_linux_c_callback_stack_offsets_remain_sysv() { + let target = Target::new(Platform::Linux, Arch::X86_64); + + assert_eq!(c_callback_stack_arg_offset(target, 6), 16); + assert_eq!(c_callback_stack_arg_offset(target, 16), 96); +} diff --git a/src/codegen_support/abi/tests/symbols.rs b/src/codegen_support/abi/tests/symbols.rs index bb55a8cc53..84e8381f58 100644 --- a/src/codegen_support/abi/tests/symbols.rs +++ b/src/codegen_support/abi/tests/symbols.rs @@ -57,3 +57,37 @@ fn test_emit_extern_symbol_address_uses_got_relocations_on_aarch64() { ) ); } + +/// Verifies `emit_extern_symbol_address` on windows-x86_64 emits a GOT-free, +/// direct RIP-relative LEA instead of the ELF-style `@GOTPCREL` load: PE/COFF +/// has no GOTPCREL relocation, and MinGW's assembler treats `@GOTPCREL` as a +/// literal name suffix rather than a relocation, producing an undefined +/// reference at link time. These "extern" symbols resolve against a +/// statically-linked `.a` archive built into the same image rather than a +/// DLL, so no `__imp_` indirection is needed either. +#[test] +fn test_emit_extern_symbol_address_uses_rip_relative_lea_on_windows_x86_64() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + crate::codegen_support::abi::symbols::emit_extern_symbol_address(&mut emitter, "r9", "demo_extern"); + let asm = emitter.output(); + + assert_eq!(asm, " lea r9, [rip + demo_extern]\n"); + assert!( + !asm.contains("GOTPCREL"), + "windows PE/COFF must never emit @GOTPCREL (MinGW treats it as a literal suffix, not a relocation)" + ); +} + +/// Verifies `emit_extern_symbol_address` on linux-x86_64 stays byte-identical +/// to before the windows-x86_64 GOT-free LEA arm was added: it must still +/// emit the exact ELF `@GOTPCREL[rip]` form. +#[test] +fn test_emit_extern_symbol_address_stays_gotpcrel_on_linux_x86_64_after_windows_fix() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + crate::codegen_support::abi::symbols::emit_extern_symbol_address(&mut emitter, "r9", "demo_extern"); + + assert_eq!( + emitter.output(), + " mov r9, QWORD PTR demo_extern@GOTPCREL[rip]\n" + ); +} diff --git a/src/codegen_support/callable_invoker_args.rs b/src/codegen_support/callable_invoker_args.rs index 7638530562..53d22d853d 100644 --- a/src/codegen_support/callable_invoker_args.rs +++ b/src/codegen_support/callable_invoker_args.rs @@ -75,8 +75,8 @@ pub(crate) fn emit_clone_indexed_array_for_invoker( 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 array_arg_reg = abi::runtime_helper_int_arg_reg(emitter, 0); + let tag_arg_reg = abi::runtime_helper_int_arg_reg(emitter, 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 @@ -101,8 +101,8 @@ 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 array_arg_reg = abi::runtime_helper_int_arg_reg(emitter, 0); + let tag_arg_reg = abi::runtime_helper_int_arg_reg(emitter, 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 @@ -149,7 +149,7 @@ pub(crate) fn emit_clone_assoc_array_for_invoker_with_value_type( value_ty: &PhpType, emitter: &mut Emitter, ) { - let hash_arg_reg = abi::int_arg_reg_name(emitter.target, 0); + let hash_arg_reg = abi::runtime_helper_int_arg_reg(emitter, 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 diff --git a/src/codegen_support/cdylib.rs b/src/codegen_support/cdylib.rs index f875e2252d..ab7146d32b 100644 --- a/src/codegen_support/cdylib.rs +++ b/src/codegen_support/cdylib.rs @@ -8,22 +8,23 @@ //! - `crate::codegen::finalize_user_asm()` when `emit == Emit::Cdylib`. //! //! Key details: -//! - elephc's internal calling convention already routes integer/scalar params -//! through the same SysV/AAPCS integer-arg registers C callers populate, and -//! PHP `Str` params arrive as a `(ptr, len)` pair in two consecutive integer -//! registers — exactly what a C caller passing `const char*, size_t` produces. -//! That alignment means the trampoline can be a single tail-branch into the -//! internal `_fn_` symbol for every signature in the v1 scalar set. +//! - SysV/AAPCS exports can tail-branch because their scalar register routing +//! matches elephc's internal ABI. Windows x86_64 instead needs an adapter: +//! MSx64 shares four positional integer/float slots while elephc tracks both +//! register classes independently, and C strings are flattened pointer/length +//! parameters that may straddle the register/stack boundary. //! - Lifecycle exports are v1 stubs: the runtime object pulled in by the //! compiled artifact uses BSS-zero-init for allocator state, so `elephc_init` //! reports success without additional work. `elephc_free` is a no-op until //! string-return marshaling lands and gives the host elephc-owned pointers //! to release. +use crate::codegen_support::abi; use crate::codegen_support::emit::Emitter; -use crate::codegen_support::platform::{Arch, Target}; +use crate::codegen_support::platform::{Arch, Platform, Target}; use crate::exports::ExportedFunction; use crate::names::function_symbol; +use crate::types::PhpType; /// Emits a `.globl ` trampoline for every exported function and the /// four lifecycle symbols. Called once after user function bodies have been @@ -34,22 +35,217 @@ pub(crate) fn emit_cdylib_exports( exports: &[&ExportedFunction], ) { for export in exports { - emit_export_trampoline(emitter, target, &export.name); + emit_export_trampoline(emitter, target, export); } emit_lifecycle_exports(emitter, target); + if target.platform == Platform::Windows { + emit_windows_cdylib_entry_stub(emitter); + emit_windows_export_directives(emitter, target, exports); + } } /// Emits a single `#[Export]` trampoline. The exported symbol receives C-ABI /// arguments in the standard SysV / AAPCS registers; we forward them unchanged /// to the internal elephc function symbol with a tail-branch so the internal /// function's `ret` returns directly to the C caller. -fn emit_export_trampoline(emitter: &mut Emitter, target: Target, php_name: &str) { - let internal = function_symbol(php_name); - let exported = target.extern_symbol(php_name); +fn emit_export_trampoline(emitter: &mut Emitter, target: Target, export: &ExportedFunction) { + let internal = function_symbol(&export.name); + let exported = target.extern_symbol(&export.name); emitter.blank(); - emitter.comment(&format!("#[Export] trampoline for PHP function {}", php_name)); + emitter.comment(&format!("#[Export] trampoline for PHP function {}", export.name)); emitter.label_global(&exported); - emit_tail_branch(emitter, target, &internal); + if (target.platform, target.arch) == (Platform::Windows, Arch::X86_64) { + emit_windows_x86_64_export_adapter(emitter, target, export, &internal); + } else { + emit_tail_branch(emitter, target, &internal); + } +} + +/// Adapts the positional MSx64 C register file to elephc's internal split +/// integer/float register files, including flattened `(pointer, length)` string +/// arguments and both ABIs' stack-overflow areas. +fn emit_windows_x86_64_export_adapter( + emitter: &mut Emitter, + target: Target, + export: &ExportedFunction, + internal: &str, +) { + let arg_types: Vec = export + .sig + .params + .iter() + .map(|(_, ty)| ty.codegen_repr()) + .collect(); + let internal_assignments = + abi::build_outgoing_arg_assignments_for_target(target, &arg_types, 0); + let flattened_units: usize = arg_types.iter().map(PhpType::register_count).sum(); + let arg_spill_bytes = flattened_units * 8; + let xmm_save_base = align16(arg_spill_bytes + 32); + let local_bytes = xmm_save_base + 9 * 16; + + emitter.instruction("push rbp"); // preserve the C caller's frame pointer + emitter.instruction("mov rbp, rsp"); // establish stable access to incoming C stack arguments + emitter.instruction(&format!("sub rsp, {}", local_bytes)); // reserve aligned spill slots for every flattened C argument + abi::store_at_offset(emitter, "rdi", arg_spill_bytes + 8); + abi::store_at_offset(emitter, "rsi", arg_spill_bytes + 16); + for xmm_index in 6..=15 { + let offset = xmm_save_base + (xmm_index - 6) * 16; + emitter.instruction(&format!( // preserve an MSx64 nonvolatile vector register across the internal call + "movdqu XMMWORD PTR [rbp - {}], xmm{}", + offset, xmm_index + )); + } + + let mut c_slot = 0usize; + let mut local_unit = 0usize; + for ty in &arg_types { + match ty { + PhpType::Float => { + spill_windows_c_unit(emitter, true, c_slot, local_unit); + c_slot += 1; + local_unit += 1; + } + PhpType::Str | PhpType::TaggedScalar => { + spill_windows_c_unit(emitter, false, c_slot, local_unit); + spill_windows_c_unit(emitter, false, c_slot + 1, local_unit + 1); + c_slot += 2; + local_unit += 2; + } + PhpType::Void | PhpType::Never => {} + _ => { + spill_windows_c_unit(emitter, false, c_slot, local_unit); + c_slot += 1; + local_unit += 1; + } + } + } + + let overflow_count = internal_assignments + .iter() + .filter(|assignment| !assignment.in_register()) + .count(); + let outgoing_bytes = 32 + overflow_count * 16; + emitter.instruction(&format!("sub rsp, {}", outgoing_bytes)); // reserve MSx64 shadow space plus internal overflow slots + + local_unit = 0; + let mut overflow_offset = 32usize; + for (ty, assignment) in arg_types.iter().zip(internal_assignments.iter()) { + if assignment.in_register() { + match ty { + PhpType::Float => { + let dst = abi::float_arg_reg_name(target, assignment.start_reg); + abi::load_at_offset(emitter, dst, export_spill_offset(local_unit)); + } + PhpType::Str | PhpType::TaggedScalar => { + let low = abi::int_arg_reg_name(target, assignment.start_reg); + let high = abi::int_arg_reg_name(target, assignment.start_reg + 1); + abi::load_at_offset(emitter, low, export_spill_offset(local_unit)); + abi::load_at_offset(emitter, high, export_spill_offset(local_unit + 1)); + } + PhpType::Void | PhpType::Never => {} + _ => { + let dst = abi::int_arg_reg_name(target, assignment.start_reg); + abi::load_at_offset(emitter, dst, export_spill_offset(local_unit)); + } + } + } else { + match ty { + PhpType::Float => { + abi::load_at_offset(emitter, "xmm15", export_spill_offset(local_unit)); + abi::emit_store_to_sp(emitter, "xmm15", overflow_offset); + } + PhpType::Str | PhpType::TaggedScalar => { + abi::load_at_offset(emitter, "r10", export_spill_offset(local_unit)); + abi::load_at_offset(emitter, "r11", export_spill_offset(local_unit + 1)); + abi::emit_store_to_sp(emitter, "r10", overflow_offset); + abi::emit_store_to_sp(emitter, "r11", overflow_offset + 8); + } + PhpType::Void | PhpType::Never => {} + _ => { + abi::load_at_offset(emitter, "r10", export_spill_offset(local_unit)); + abi::emit_store_to_sp(emitter, "r10", overflow_offset); + } + } + overflow_offset += 16; + } + local_unit += ty.register_count(); + } + + emitter.instruction(&format!("call {}", internal)); // invoke the internal PHP body with elephc's ABI + emitter.instruction(&format!("add rsp, {}", outgoing_bytes)); // release shadow space and internal overflow slots + for xmm_index in (6..=15).rev() { + let offset = xmm_save_base + (xmm_index - 6) * 16; + emitter.instruction(&format!( // restore an MSx64 nonvolatile vector register after the internal call + "movdqu xmm{}, XMMWORD PTR [rbp - {}]", + xmm_index, offset + )); + } + abi::load_at_offset(emitter, "rsi", arg_spill_bytes + 16); + abi::load_at_offset(emitter, "rdi", arg_spill_bytes + 8); + emitter.instruction(&format!("add rsp, {}", local_bytes)); // release flattened C argument spill slots + emitter.instruction("pop rbp"); // restore the C caller's frame pointer + emitter.instruction("ret"); // return the scalar result through the matching C result register +} + +/// Spills one flattened MSx64 C argument unit into a stable local slot. +fn spill_windows_c_unit(emitter: &mut Emitter, is_float: bool, c_slot: usize, local_unit: usize) { + let local_offset = export_spill_offset(local_unit); + if c_slot < 4 { + let reg = if is_float { + abi::float_arg_reg_name(emitter.target, c_slot) + } else { + abi::int_arg_reg_name(emitter.target, c_slot) + }; + abi::store_at_offset(emitter, reg, local_offset); + } else { + let scratch = if is_float { "xmm15" } else { "r10" }; + abi::load_from_caller_stack(emitter, scratch, 48 + (c_slot - 4) * 8); + abi::store_at_offset(emitter, scratch, local_offset); + } +} + +/// Returns the rbp-relative local offset for one flattened export argument unit. +fn export_spill_offset(unit: usize) -> usize { + (unit + 1) * 8 +} + +/// Emits PE linker directives that export only the public API symbols and keep +/// internal runtime globals out of the DLL export table. +fn emit_windows_export_directives( + emitter: &mut Emitter, + target: Target, + exports: &[&ExportedFunction], +) { + emitter.blank(); + emitter.raw(".section .drectve"); + for export in exports { + emitter.raw(&format!( + ".ascii \" -export:{}\"", + target.extern_symbol(&export.name) + )); + } + for name in [ + "elephc_init", + "elephc_shutdown", + "elephc_last_error", + "elephc_free", + ] { + emitter.raw(&format!( + ".ascii \" -export:{}\"", + target.extern_symbol(name) + )); + } +} + +/// Satisfies the runtime object's executable-entry shim when linking it into a +/// DLL. A cdylib has no PHP top-level entry body, and this private zero-returning +/// symbol is deliberately absent from the PE export directives. +fn emit_windows_cdylib_entry_stub(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("private cdylib entry stub for the shared Windows runtime object"); + emitter.label_global(emitter.entry_symbol()); + emitter.instruction("xor eax, eax"); // report a no-op top-level body if the unused CRT shim is reached + emitter.instruction("ret"); // return from the private DLL-only entry stub } /// Emits the four C-callable lifecycle symbols required for a v1 cdylib host @@ -111,3 +307,8 @@ fn emit_tail_branch(emitter: &mut Emitter, target: Target, target_symbol: &str) Arch::X86_64 => emitter.instruction(&format!("jmp {}", target_symbol)), // tail-call the internal PHP function body } } + +/// Rounds a byte count up to the stack's required sixteen-byte alignment. +fn align16(bytes: usize) -> usize { + (bytes + 15) & !15 +} diff --git a/src/codegen_support/emit.rs b/src/codegen_support/emit.rs index 134339ddd2..f507709ac5 100644 --- a/src/codegen_support/emit.rs +++ b/src/codegen_support/emit.rs @@ -111,12 +111,16 @@ impl Emitter { /// Emit a label that is visible across object files (for two-object linking). /// On Linux, places each global symbol in its own `.text.` section so /// that `--gc-sections` can eliminate unreachable helpers at link time. + /// On Windows, emits `.globl` only (PE/COFF does not support per-function sections via GAS). pub fn label_global(&mut self, name: &str) { if self.platform == Platform::Linux { let _ = writeln!(self.buf, ".section .text.{},\"ax\",@progbits", name); let _ = writeln!(self.buf, ".globl {}", name); let _ = writeln!(self.buf, ".type {}, %function", name); let _ = writeln!(self.buf, "{}:", name); + } else if self.platform == Platform::Windows { + let _ = writeln!(self.buf, ".globl {}", name); + let _ = writeln!(self.buf, "{}:", name); } else { let _ = writeln!(self.buf, ".globl {}", name); let _ = writeln!(self.buf, "{}:", name); @@ -234,43 +238,270 @@ impl Emitter { let target = self.target; target.emit_linux_syscall(self, macos_num); } - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => panic!("Windows does not use AArch64 syscalls (see issue #379)"), } } // ── Platform-aware C symbol call ───────────────────────────────── /// Emit `bl _func` (macOS) or `bl func` (Linux) for C library calls. + /// + /// For a symbol that may be an imported Windows API function + /// (msvcrt/ws2_32 — anything a `WIN32_IMPORTS` entry could name), prefer + /// [`Emitter::emit_call_c`]: on windows-x86_64 `bl_c` always emits a bare + /// `call func`, which is only correct for internal/toolchain symbols — + /// a bare msvcrt/ws2_32 import expects the MSx64 calling convention, not + /// SysV, so calling it through `bl_c` is the Class-1 ABI bug. `bl_c` + /// remains the right choice for symbols that never resolve to a Windows + /// import. 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::Windows, Arch::X86_64) => { + debug_assert!( + super::runtime::windows_c_shim_name(func).is_none(), + "bl_c(\"{func}\") on windows-x86_64 targets a symbol with a registered \ + __rt_sys_* shim; call emit_call_c(\"{func}\") instead so the shim's \ + SysV->MSx64 ABI conversion is applied rather than a raw SysV-staged import call" + ); + // MinGW's C-library setjmp/longjmp are SEH-based and read their + // arguments MSx64-style; elephc's SysV-staged, SEH-free replacements + // (see runtime::exceptions::setjmp) are used on this target instead. + let name = match func { + "setjmp" => "__rt_setjmp", + "longjmp" => "__rt_longjmp", + other => other, + }; + self.instruction(&format!("call {}", name)); + } (Platform::MacOS, Arch::X86_64) => { panic!("C symbol calls are not implemented yet for target macos-x86_64"); } - (Platform::Windows, _) => panic!("Windows target is not yet supported (see issue #379)"), + (Platform::Windows, Arch::AArch64) => { + panic!("Windows ARM64 target is not yet supported (see issue #379)"); + } + } + } + + /// Symbols for which, on windows-x86_64, `emit_shim_c_symbols` / + /// `emit_shim_c_symbol_delegates` (`codegen_support::runtime::win32`) + /// emit an internal label of the identical name that performs its own + /// SysV→MSx64 ABI conversion internally (e.g. `write`, `read`, `stat`). + /// A bare `call ` for one of these reaches that internal label, + /// not the msvcrt/ws2_32 import of the same name, so it is safe for + /// [`Emitter::emit_call_c`] to emit it unchanged. Keep this list in sync + /// with the labels those two functions emit. + const EMIT_CALL_C_SYSV_STUB_DELEGATES: &'static [&'static str] = &[ + "accept4", "access", "brk", "chdir", "chmod", "clock_gettime", "close", "dirfd", + "execve", "exit", "fcntl", "fgetc", "fileno", "flock", "fnmatch", "fstat", "fsync", + "ftruncate", "futex", "getcwd", "getpid", "getrandom", "glob", "globfree", "h_errno", + "hstrerror", "ioctl", "kill", "link", "lseek", "lstat", "main", "mkdir", "mmap", + "mprotect", "munmap", "open", "pclose", "popen", "read", "readlink", "realpath", + "rename", "rmdir", "sleep", "stat", "symlink", "sysinfo", "system", "timegm", "umask", + "uname", "unlink", "usleep", "utimensat", "write", "writev", + ]; + + /// Emit a call to a C-library symbol that may be an imported Windows API + /// function (msvcrt/ws2_32). Contrast with [`Emitter::bl_c`]: `bl_c` + /// emits a bare `call func` on windows-x86_64 unconditionally, which is + /// wrong whenever `func` is a bare msvcrt/ws2_32 import (entered with + /// SysV registers instead of the MSx64 ABI it expects — the Class-1 ABI + /// bug). `emit_call_c` routes `symbol` correctly on windows-x86_64: + /// - if `symbol` has a registered `__rt_sys_` shim (the registry + /// is `codegen_support::runtime::win32::windows_c_shim_name`, the + /// single source of truth for Windows C shims), emits + /// `call __rt_sys_`; + /// - else, if `symbol` is a known SysV stub-delegate (see + /// [`Self::EMIT_CALL_C_SYSV_STUB_DELEGATES`]), emits a bare + /// `call ` — correct because the call target is the internal + /// stub-delegate label, not the msvcrt/ws2_32 import; + /// - else PANICS with a message naming the missing shim/stub. This is a + /// build-time exhaustiveness guard: it only fires for symbols actually + /// passed to `emit_call_c`, so a future call site added for a symbol + /// with neither a shim nor a stub blows up the first test that + /// exercises it, instead of silently reintroducing the Class-1 bug. + /// + /// On every other target, emits exactly what `bl_c` emits (`call symbol` + /// on Linux x86_64; `bl _symbol`/`bl symbol` on AArch64) — byte-identical. + pub fn emit_call_c(&mut self, symbol: &str) { + if (self.platform, self.target.arch) != (Platform::Windows, Arch::X86_64) { + self.bl_c(symbol); + return; + } + if let Some(shim) = super::runtime::windows_c_shim_name(symbol) { + self.instruction(&format!("call {}", shim)); + } else if Self::EMIT_CALL_C_SYSV_STUB_DELEGATES.contains(&symbol) { + self.instruction(&format!("call {}", symbol)); + } else { + panic!( + "emit_call_c(\"{symbol}\"): no Windows shim and not a SysV stub-delegate — \ + add a __rt_sys_{symbol} shim or register {symbol} as stub-covered" + ); + } + } + + /// Emits, on windows-x86_64 ONLY, the reverse-index-order remap of the SysV + /// integer argument registers into the MSx64 registers a GENERATED callback + /// (closure/descriptor invoker, user method, magic method) reads, immediately + /// before an indirect `call` into that generated code. Mirror of + /// `remap_platform_args_to_runtime_helper_regs` (generated→`__rt_*`, SysV): here + /// a hand-written runtime helper calls BACK into generated code, so the + /// SysV-staged args must be moved into the MSx64 argument registers. Reverse + /// index order (r9←rcx, r8←rdx, rdx←rsi, rcx←rdi) avoids clobbering a source + /// register a later index still reads (rcx is SysV arg3 AND MSx64 arg0; rdx is + /// SysV arg2 AND MSx64 arg1). No-op (nothing emitted) on every + /// non-windows-x86_64 target, so output is byte-identical there. `int_reg_count` + /// MUST be ≤4: a callback consuming >4 integer registers passes its 5th+ arg on + /// the MSx64 stack and needs bespoke stack+shadow staging, not this helper. + pub fn remap_sysv_args_to_platform_for_callback(&mut self, int_reg_count: usize) { + assert!( + int_reg_count <= 4, + "reverse-ABI callback remap covers <=4 register args; {int_reg_count} needs MSx64 stack staging" + ); + if (self.platform, self.target.arch) != (Platform::Windows, Arch::X86_64) { + return; + } + for idx in (0..int_reg_count).rev() { + let src = crate::codegen_support::abi::runtime_helper_int_arg_reg(self, idx); + let dst = crate::codegen_support::abi::int_arg_reg_name(self.target, idx); + if src != dst { + self.instruction(&format!("mov {}, {}", dst, src)); // SysV callback arg -> MSx64 reg the generated callee reads + } + } + } + + /// Calls generated PHP-ABI code from a hand-written SysV runtime helper. + /// + /// Runtime helpers always stage integer arguments in SysV registers. On + /// Windows x86_64, generated functions instead consume MSx64 registers and + /// require 32 bytes of caller shadow space. This adapter relocates the call + /// target away from argument registers, stages the fifth and sixth SysV + /// arguments in MSx64 stack slots, remaps the first four registers without + /// collisions, and restores the stack after the indirect call. Other + /// targets retain the original bare indirect-call sequence byte-for-byte. + pub fn emit_platform_callback_call(&mut self, fnptr_reg: &str, int_arg_count: usize) { + self.emit_sysv_to_msx64_indirect_call(fnptr_reg, int_arg_count, "generated PHP callback"); + } + + /// Emits a call to a NATIVE (real C/Rust, MSx64-ABI) function through the + /// pointer in `fnptr_reg`, correcting the ABI on windows-x86_64. The + /// hand-written runtime helpers stage arguments in the SysV registers; a + /// genuine native callee on windows reads the MSx64 registers, needs + /// 32-byte caller shadow space, and (at >=4 args) would collide with a + /// fn-ptr parked in r9. This relocates the fn-ptr off the MSx64 argument + /// registers into r11, reserves the shadow space (plus 8-byte slots for + /// the 5th/6th integer args, 16-byte aligned), moves the SysV 5th/6th + /// args (r8/r9) onto the stack BEFORE the register remap clobbers them, + /// remaps the first <=4 SysV integer args into the MSx64 registers + /// (reverse index order, via `remap_sysv_args_to_platform_for_callback`), + /// then calls the relocated pointer. Byte-identical to a bare + /// `call {fnptr_reg}` on every other target. Arguments must already be in + /// the SysV integer registers (rdi/rsi/rdx/rcx/r8/r9); no float args + /// (assert-free but undefined if present); `int_arg_count` must be <= 6 + /// (a 7th+ arg needs bespoke staging). The caller frame must be 16-byte + /// aligned at this point. + pub fn emit_native_bridge_call(&mut self, fnptr_reg: &str, int_arg_count: usize) { + self.emit_sysv_to_msx64_indirect_call(fnptr_reg, int_arg_count, "native bridge"); + } + + /// Calls a statically linked native C/Rust bridge symbol from a hand-written + /// runtime helper whose integer arguments use the compiler's SysV runtime ABI. + /// + /// Windows x86_64 materializes the platform-mangled symbol address and routes + /// it through [`Emitter::emit_native_bridge_call`] so arguments are remapped + /// and the mandatory MSx64 shadow space is reserved. Other targets retain a + /// direct platform-mangled C-symbol call. + pub fn emit_native_bridge_symbol_call(&mut self, symbol: &str, int_arg_count: usize) { + if (self.platform, self.target.arch) != (Platform::Windows, Arch::X86_64) { + self.bl_c(symbol); + return; + } + + let extern_symbol = self.target.extern_symbol(symbol); + crate::codegen_support::abi::emit_symbol_address(self, "r11", &extern_symbol); + self.emit_native_bridge_call("r11", int_arg_count); + } + + /// Calls an indirect bridge entry whose published pointer already exposes + /// the compiler runtime ABI, including through a Windows ABI adapter. + pub fn emit_published_bridge_call(&mut self, fnptr_reg: &str) { + match self.target.arch { + Arch::AArch64 => self.instruction(&format!("blr {fnptr_reg}")), + Arch::X86_64 => self.instruction(&format!("call {fnptr_reg}")), + } + } + + /// Emits the shared SysV-runtime to MSx64 indirect-call transition. + fn emit_sysv_to_msx64_indirect_call( + &mut self, + fnptr_reg: &str, + int_arg_count: usize, + callee_kind: &str, + ) { + if (self.platform, self.target.arch) != (Platform::Windows, Arch::X86_64) { + self.instruction(&format!("call {}", fnptr_reg)); // native call, SysV/AAPCS ABI — byte-identical off windows + return; + } + assert!( + int_arg_count <= 6, + "{callee_kind} call remap covers <=6 integer args; {int_arg_count} needs bespoke staging" + ); + if fnptr_reg != "r11" { + self.instruction(&format!("mov r11, {}", fnptr_reg)); // relocate the fn-ptr off the MSx64 argument registers + } + // 32-byte MSx64 shadow space + one 8-byte slot per 5th/6th SysV int arg, 16-byte aligned + let stack_args = int_arg_count.saturating_sub(4); + let frame = (32 + stack_args * 8 + 15) / 16 * 16; + self.instruction(&format!("sub rsp, {}", frame)); // reserve MSx64 shadow space and stack-arg slots + for idx in 4..int_arg_count { + let src = crate::codegen_support::abi::runtime_helper_int_arg_reg(self, idx); + let offset = 32 + (idx - 4) * 8; + self.instruction(&format!("mov QWORD PTR [rsp + {}], {}", offset, src)); // 5th+ SysV int arg -> MSx64 stack slot (before the remap clobbers r8/r9) } + self.remap_sysv_args_to_platform_for_callback(int_arg_count.min(4)); + self.instruction("call r11"); // invoke the MSx64 callee via the relocated pointer + self.instruction(&format!("add rsp, {}", frame)); // release the shadow + stack-arg scratch } // ── Platform-aware entry point ─────────────────────────────────── - /// Returns the program entry point symbol: `_main` (macOS) or `main` (Linux). + /// Returns the program entry point symbol: `_main` (macOS), `main` (Linux), + /// or `__elephc_main` (Windows x86_64 — the Win32 shim emits the real `main` + /// wrapper that calls into `__elephc_main`). pub fn entry_symbol(&self) -> &'static str { match self.target.arch { Arch::AArch64 => match self.platform { Platform::MacOS => "_main", Platform::Linux => "main", - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => { + panic!("Windows ARM64 target is not yet supported (see issue #379)") + } + }, + Arch::X86_64 => match self.platform { + Platform::Windows => "__elephc_main", + _ => "main", }, - Arch::X86_64 => "main", } } - /// Emit the program entry point label: `_main` (macOS) or `main` (Linux). + /// Emit the program entry point label: `_main` (macOS), `main` (Linux), + /// or `__elephc_main` (Windows — the Win32 shim emits the real `main` wrapper). pub fn entry_label(&mut self) { - let symbol = self.entry_symbol(); - self.label_global(symbol); + match self.target.arch { + Arch::AArch64 => match self.platform { + Platform::MacOS => self.label_global("_main"), + Platform::Linux => self.label_global("main"), + Platform::Windows => { + panic!("Windows ARM64 target is not yet supported (see issue #379)"); + } + }, + Arch::X86_64 => match self.platform { + Platform::Windows => self.label_global("__elephc_main"), + _ => self.label_global("main"), + }, + } } } @@ -316,8 +547,24 @@ pub fn localize_internal_labels(asm: &str, internal: &HashSet) -> String #[cfg(test)] mod tests { + use std::collections::BTreeMap; + use std::fs; + use std::path::{Path, PathBuf}; + use super::*; + /// Collects Rust source files below one runtime directory for structural ABI checks. + fn collect_runtime_rust_files(dir: &Path, files: &mut Vec) { + for entry in fs::read_dir(dir).expect("runtime source directory must be readable") { + let path = entry.expect("runtime source entry must be readable").path(); + if path.is_dir() { + collect_runtime_rust_files(&path, files); + } else if path.extension().is_some_and(|extension| extension == "rs") { + files.push(path); + } + } + } + /// Verifies comment prefix is platform aware. #[test] fn test_comment_prefix_is_platform_aware() { @@ -345,4 +592,305 @@ mod tests { linux_x86.emit_text_prelude(); assert_eq!(linux_x86.output(), ".intel_syntax noprefix\n.text\n"); } + + /// Verifies that on windows-x86_64 the reverse-ABI callback remap emits every + /// SysV-to-MSx64 `mov` for a 4-register-arg indirect call into generated code. + #[test] + fn test_reverse_remap_emits_sysv_to_msx64_on_windows_x86_64() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emitter.remap_sysv_args_to_platform_for_callback(4); + let asm = emitter.output(); + assert!(asm.contains("mov rcx, rdi")); + assert!(asm.contains("mov rdx, rsi")); + assert!(asm.contains("mov r8, rdx")); + assert!(asm.contains("mov r9, rcx")); + } + + /// Verifies the remap is emitted in REVERSE index order so a later move never + /// clobbers a register an earlier-index source still needs to read: `r9 <- rcx` + /// and `r8 <- rdx` must appear before `rcx <- rdi` and `rdx <- rsi` overwrite + /// the `rcx`/`rdx` values those higher-index moves still need to read from. + #[test] + fn test_reverse_remap_orders_high_index_moves_before_low_index_clobbers() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emitter.remap_sysv_args_to_platform_for_callback(4); + let asm = emitter.output(); + let r9_idx = asm.find("mov r9, rcx").expect("expected mov r9, rcx"); + let rcx_idx = asm.find("mov rcx, rdi").expect("expected mov rcx, rdi"); + assert!(r9_idx < rcx_idx, "mov r9, rcx must precede mov rcx, rdi"); + let r8_idx = asm.find("mov r8, rdx").expect("expected mov r8, rdx"); + let rdx_idx = asm.find("mov rdx, rsi").expect("expected mov rdx, rsi"); + assert!(r8_idx < rdx_idx, "mov r8, rdx must precede mov rdx, rsi"); + } + + /// Verifies a single-register-arg remap on windows-x86_64 emits exactly one + /// `mov` and nothing referencing the unused rdx/r8/r9 registers. + #[test] + fn test_reverse_remap_single_register_arg_on_windows_x86_64() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emitter.remap_sysv_args_to_platform_for_callback(1); + let asm = emitter.output(); + assert_eq!(asm, " mov rcx, rdi\n"); + assert!(!asm.contains("rdx")); + assert!(!asm.contains("r8")); + assert!(!asm.contains("r9")); + } + + /// Verifies the remap is a no-op on linux-x86_64: `runtime_helper_int_arg_reg` + /// and `int_arg_reg_name` already agree there, so `src == dst` for every index + /// and the output is byte-identical to not calling the remap at all. + #[test] + fn test_reverse_remap_is_noop_on_linux_x86_64() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emitter.remap_sysv_args_to_platform_for_callback(4); + let asm = emitter.output(); + assert!(!asm.contains("mov")); + } + + /// Verifies the remap emits nothing on macOS AArch64: AArch64 has a single + /// calling convention, so this windows-x86_64-only helper is a pure no-op. + #[test] + fn test_reverse_remap_is_noop_on_aarch64() { + let mut emitter = Emitter::new(Target::new(Platform::MacOS, Arch::AArch64)); + emitter.remap_sysv_args_to_platform_for_callback(2); + let asm = emitter.output(); + assert!(asm.is_empty()); + } + + /// Verifies a <=4-arg native-bridge call on windows-x86_64 relocates the + /// fn-ptr to r11, reserves the 32-byte shadow space, remaps the SysV args + /// into the MSx64 registers, calls through r11, and releases the shadow + /// space — and never emits a bare `call r9` (the pre-F46 bug: a real + /// native callee would read MSx64 registers it was never given). + #[test] + fn test_native_bridge_call_windows_x86_64_two_args() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emitter.emit_native_bridge_call("r9", 2); + let asm = emitter.output(); + assert!(asm.contains("mov r11, r9")); + assert!(asm.contains("sub rsp, 32")); + assert!(asm.contains("mov rcx, rdi")); + assert!(asm.contains("mov rdx, rsi")); + assert!(asm.contains("call r11")); + assert!(asm.contains("add rsp, 32")); + assert!(!asm.contains("call r9\n")); + } + + /// Verifies a direct native bridge symbol receives the same Windows ABI + /// transition as an indirect published bridge pointer. + #[test] + fn test_native_bridge_symbol_call_windows_x86_64_two_args() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emitter.emit_native_bridge_symbol_call("elephc_web_write", 2); + let asm = emitter.output(); + assert!(asm.contains("lea r11, [rip + elephc_web_write]")); + assert!(asm.contains("sub rsp, 32")); + assert!(asm.contains("mov rcx, rdi")); + assert!(asm.contains("mov rdx, rsi")); + assert!(asm.contains("call r11")); + assert!(asm.contains("add rsp, 32")); + assert!(!asm.contains("call elephc_web_write")); + } + + /// Verifies zero-argument native calls still reserve Windows shadow space. + #[test] + fn test_native_bridge_symbol_call_windows_x86_64_zero_args() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emitter.emit_native_bridge_symbol_call("elephc_web_body_len", 0); + let asm = emitter.output(); + assert!(asm.contains("lea r11, [rip + elephc_web_body_len]")); + assert!(asm.contains("sub rsp, 32")); + assert!(asm.contains("call r11")); + assert!(asm.contains("add rsp, 32")); + assert!(!asm.contains("mov rcx")); + } + + /// Verifies Linux keeps the original direct symbol call without Windows staging. + #[test] + fn test_native_bridge_symbol_call_is_direct_on_linux_x86_64() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emitter.emit_native_bridge_symbol_call("elephc_web_write", 2); + assert_eq!(emitter.output(), " call elephc_web_write\n"); + } + + /// Guards web runtime exports against bypassing the native-symbol ABI helper. + #[test] + fn test_web_runtime_native_symbols_do_not_use_bare_bl_c() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + for (relative, expected_calls) in [ + ("src/codegen_support/runtime/io/http_response.rs", 4usize), + ("src/codegen_support/runtime/io/stdout_write.rs", 2usize), + ("src/codegen_support/runtime/io/php_input.rs", 4usize), + ] { + let source = fs::read_to_string(root.join(relative)) + .expect("web runtime bridge source must be readable"); + assert_eq!( + source.matches("emit_native_bridge_symbol_call").count(), + expected_calls, + "{relative} must route every web export through the native-symbol ABI helper" + ); + assert!( + !source.contains("bl_c(\"elephc_web_"), + "{relative} must not call a native web export through bare bl_c" + ); + } + } + + /// Verifies a generated callback transition reserves MSx64 shadow space, + /// remaps SysV registers, and relocates the indirect target. + #[test] + fn test_platform_callback_call_windows_x86_64_two_args() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emitter.emit_platform_callback_call("r12", 2); + let asm = emitter.output(); + assert!(asm.contains("mov r11, r12")); + assert!(asm.contains("sub rsp, 32")); + assert!(asm.contains("mov rcx, rdi")); + assert!(asm.contains("mov rdx, rsi")); + assert!(asm.contains("call r11")); + assert!(asm.contains("add rsp, 32")); + assert!(!asm.contains("call r12\n")); + } + + /// Verifies generated callback overflow arguments use the fifth and sixth + /// MSx64 ABI stack slots after the mandatory shadow space. + #[test] + fn test_platform_callback_call_windows_x86_64_six_slots_stages_overflow() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emitter.emit_platform_callback_call("r12", 6); + let asm = emitter.output(); + assert!(asm.contains("sub rsp, 48")); + assert!(asm.contains("mov QWORD PTR [rsp + 32], r8")); + assert!(asm.contains("mov QWORD PTR [rsp + 40], r9")); + assert!(asm.contains("call r11")); + assert!(asm.contains("add rsp, 48")); + } + + /// Verifies Linux retains the original bare indirect callback call. + #[test] + fn test_platform_callback_call_is_bare_call_on_linux_x86_64() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emitter.emit_platform_callback_call("r12", 6); + assert_eq!(emitter.output(), " call r12\n"); + } + + /// Rejects new hand-written runtime indirect calls unless their ABI family + /// is explicitly classified here and has its own staging regression test. + #[test] + fn test_runtime_indirect_calls_are_structurally_allowlisted() { + let runtime_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/codegen_support/runtime"); + let allowed = BTreeMap::from([ + (("io/fopen.rs", "r11"), 2usize), // generated stream_open, bespoke seven-slot staging + (("io/fwrite.rs", "r9"), 3usize), // internal compression and iconv filter entries + (("io/ob_handler.rs", "r10"), 1usize), // internal output-handler stubs always use the runtime SysV ABI + (("io/user_wrapper_path_op.rs", "r11"), 2usize), // generated rename wrapper, bespoke composite-string staging + (("strings/hash_hmac.rs", "r11"), 1usize), // native seven-slot crypto bridge on Windows + (("strings/hash_hmac.rs", "rax"), 1usize), // byte-identical native crypto bridge off Windows + ]); + let mut files = Vec::new(); + collect_runtime_rust_files(&runtime_root, &mut files); + let mut observed = BTreeMap::<(String, String), usize>::new(); + + for path in files { + let relative = path + .strip_prefix(&runtime_root) + .expect("collected runtime source must remain below its root") + .to_string_lossy() + .replace('\\', "/"); + let source = fs::read_to_string(&path).expect("runtime Rust source must be readable"); + for line in source.lines() { + let Some(call) = line.split("emitter.instruction(\"call ").nth(1) else { + continue; + }; + let Some(register) = call.split('"').next() else { + continue; + }; + if !matches!( + register, + "rax" | "rbx" | "rcx" | "rdx" | "rsi" | "rdi" | "r8" | "r9" | "r10" + | "r11" | "r12" | "r13" | "r14" | "r15" + ) { + continue; + } + *observed.entry((relative.clone(), register.to_string())).or_default() += 1; + } + } + + let allowed_owned = allowed + .into_iter() + .map(|((path, register), count)| ((path.to_string(), register.to_string()), count)) + .collect::>(); + assert_eq!( + observed, allowed_owned, + "new runtime indirect calls must use emit_platform_callback_call/emit_native_bridge_call or be explicitly classified" + ); + } + + /// Guards eval callable-to-magician transitions against reintroducing bare SysV calls on Windows. + #[test] + fn test_eval_callable_magician_calls_use_native_bridge_on_windows() { + let path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("src/codegen/eval_callable_helpers.rs"); + let source = fs::read_to_string(path).expect("eval callable helper source must be readable"); + + assert_eq!( + source.matches("emitter.emit_native_bridge_call(\"r11\",").count(), + 2, + "both x86_64 magician callbacks must cross the target-native C ABI bridge" + ); + assert_eq!( + source.matches("abi::emit_symbol_address(emitter, \"r11\",").count(), + 2, + "magician function addresses must not be loaded as data words" + ); + assert!( + source.contains("abi::int_arg_reg_name(emitter.target, 0)") + && source.contains("abi::int_arg_reg_name(emitter.target, 1)"), + "dynamic invokers must save incoming descriptor arguments from target ABI registers" + ); + } + + /// Verifies a 5-arg native-bridge call on windows-x86_64 additionally + /// reserves an 8-byte stack slot for the 5th SysV int arg (32+8 -> 48, + /// 16-byte aligned) and stages that 5th arg (r8) onto the MSx64 stack + /// BEFORE the register remap clobbers r8 with the 3rd MSx64 arg. + #[test] + fn test_native_bridge_call_windows_x86_64_five_args_stages_stack_arg_before_remap() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emitter.emit_native_bridge_call("r9", 5); + let asm = emitter.output(); + assert!(asm.contains("sub rsp, 48")); + let stage_idx = asm + .find("mov QWORD PTR [rsp + 32], r8") + .expect("expected the 5th SysV int arg staged to the MSx64 stack slot"); + let remap_idx = asm + .find("mov r8, rdx") + .expect("expected the remap's mov r8, rdx (MSx64 arg2 <- SysV arg2)"); + assert!( + stage_idx < remap_idx, + "the 5th-arg stack stage must precede the remap's mov r8, rdx clobber" + ); + assert!(asm.contains("call r11")); + } + + /// Verifies the native-bridge call is byte-identical to a bare `call r9` + /// on linux-x86_64 — no remap, no shadow space, no relocation. + #[test] + fn test_native_bridge_call_is_bare_call_on_linux_x86_64() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emitter.emit_native_bridge_call("r9", 5); + let asm = emitter.output(); + assert_eq!(asm, " call r9\n"); + } + + /// Verifies the native-bridge call is byte-identical to a bare `call x9` + /// on macOS AArch64 — AArch64 has a single calling convention, so this + /// windows-x86_64-only correction is a pure no-op there. + #[test] + fn test_native_bridge_call_is_bare_call_on_macos_aarch64() { + let mut emitter = Emitter::new(Target::new(Platform::MacOS, Arch::AArch64)); + emitter.emit_native_bridge_call("x9", 2); + let asm = emitter.output(); + assert_eq!(asm, " call x9\n"); + } } diff --git a/src/codegen_support/hash_crypto.rs b/src/codegen_support/hash_crypto.rs index d3afc8243b..6b4863b0ab 100644 --- a/src/codegen_support/hash_crypto.rs +++ b/src/codegen_support/hash_crypto.rs @@ -24,7 +24,7 @@ //! per-program `DataSection`. use crate::codegen_support::emit::Emitter; -use crate::codegen_support::{abi, platform::Arch}; +use crate::codegen_support::{abi, platform::Arch, platform::Platform}; /// 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` @@ -108,6 +108,13 @@ fn emit_throw_value_error_aarch64(emitter: &mut Emitter, message_symbol: &str, m /// 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) { + if emitter.platform == Platform::Windows { + abi::emit_load_symbol_to_reg(emitter, "rdi", "_spl_value_error_class_id", 0); // pass ValueError's runtime class id + abi::emit_symbol_address(emitter, "rsi", message_symbol); // pass the static ValueError message pointer + emitter.instruction(&format!("mov rdx, {}", message_len)); // pass the static ValueError message length + emitter.instruction("jmp __rt_throw_static_exception"); // allocate and publish through an independent unwind frame + return; + } 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 diff --git a/src/codegen_support/mod.rs b/src/codegen_support/mod.rs index 6e48ca8008..4330df9a2c 100644 --- a/src/codegen_support/mod.rs +++ b/src/codegen_support/mod.rs @@ -32,6 +32,7 @@ pub(crate) mod sentinels; pub(crate) mod stream_filters; pub(crate) mod tls; pub(crate) mod try_handlers; +pub(crate) mod tz_bridge; mod value_boxing; pub(crate) mod visibility; mod wrappers; diff --git a/src/codegen_support/platform/mod.rs b/src/codegen_support/platform/mod.rs index a3fa51641d..a64dcd1156 100644 --- a/src/codegen_support/platform/mod.rs +++ b/src/codegen_support/platform/mod.rs @@ -11,8 +11,10 @@ mod linux_transform; mod target; mod toolchain; +mod windows_transform; pub use target::{Arch, Platform, Target}; +pub use windows_transform::transform_for_windows; #[cfg(test)] mod tests { @@ -37,6 +39,14 @@ mod tests { Target::parse("aarch64-apple-darwin").unwrap(), Target::new(Platform::MacOS, Arch::AArch64) ); + assert_eq!( + Target::parse("x86_64-pc-windows-gnu").unwrap(), + Target::new(Platform::Windows, Arch::X86_64) + ); + assert_eq!( + Target::parse("x86_64-pc-windows-msvc").unwrap(), + Target::new(Platform::Windows, Arch::X86_64) + ); } #[test] diff --git a/src/codegen_support/platform/target.rs b/src/codegen_support/platform/target.rs index 0cc8914d12..387dc42086 100644 --- a/src/codegen_support/platform/target.rs +++ b/src/codegen_support/platform/target.rs @@ -10,6 +10,7 @@ use super::linux_transform::{map_syscall, needs_at_fdcwd, transform_for_linux}; use super::toolchain::host_has_native_aarch64_toolchain; +use super::windows_transform::transform_for_windows; /// Target platform for code generation. /// @@ -41,7 +42,8 @@ pub struct Target { impl Platform { /// Detects the host operating system from the Rust compile-time target OS. /// - /// Returns `Platform::MacOS` when compiling on macOS, otherwise `Platform::Linux`. + /// Returns `Platform::MacOS` when compiling on macOS, `Platform::Windows` on Windows, + /// otherwise `Platform::Linux`. pub fn detect_host() -> Self { if cfg!(target_os = "macos") { Platform::MacOS @@ -63,6 +65,51 @@ impl Platform { } } + /// Returns the PHP-compatible OS family string for this platform. + /// + /// Windows reports `"Windows"`, macOS reports `"Darwin"`, and Linux reports + /// `"Linux"`, matching PHP's `PHP_OS_FAMILY` constant. + pub fn php_os_family(&self) -> &'static str { + match self { + Platform::Windows => "Windows", + Platform::MacOS => "Darwin", + Platform::Linux => "Linux", + } + } + + /// Returns the PHP-compatible end-of-line sequence for this platform. + /// + /// Windows uses `"\r\n"` (CRLF); macOS and Linux use `"\n"` (LF), matching PHP's + /// `PHP_EOL` constant. + pub fn php_eol(&self) -> &'static str { + match self { + Platform::Windows => "\r\n", + Platform::MacOS | Platform::Linux => "\n", + } + } + + /// Returns the PHP-compatible directory separator for this platform. + /// + /// Windows uses `"\\"`; macOS and Linux use `"/"`, matching PHP's + /// `DIRECTORY_SEPARATOR` constant. + pub fn directory_separator(&self) -> &'static str { + match self { + Platform::Windows => "\\", + Platform::MacOS | Platform::Linux => "/", + } + } + + /// Returns the PHP-compatible `PATH_SEPARATOR` for this platform. + /// + /// Windows uses `";"` to separate entries in `include_path`/`PATH`-like lists; + /// macOS and Linux use `":"`, matching PHP's `PATH_SEPARATOR` constant. + pub fn path_separator(&self) -> &'static str { + match self { + Platform::Windows => ";", + Platform::MacOS | Platform::Linux => ":", + } + } + /// Returns the `O_WRONLY | O_CREAT | O_TRUNC` flag combination for `open()`. /// /// These flags open a file for writing, creating it if it does not exist, @@ -72,7 +119,38 @@ impl Platform { match self { Platform::MacOS => 0x601, Platform::Linux => 0x241, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 0x241, + } + } + + /// Returns `O_EXCL`, used with `O_CREAT` for PHP's exclusive `x` fopen mode. + pub fn o_excl(&self) -> u32 { + match self { + Platform::MacOS => 0x0800, + Platform::Linux | Platform::Windows => 0x0080, + } + } + + /// Returns the optional close-on-exec flag supported by this target's open ABI. + /// + /// Windows handle inheritance is configured independently by the Win32 shim, so + /// its CRT-compatible open path deliberately has no `O_CLOEXEC` bit here. + pub fn o_cloexec(&self) -> Option { + match self { + Platform::MacOS => Some(0x0100_0000), + Platform::Linux => Some(0x0008_0000), + Platform::Windows => None, + } + } + + /// Returns the optional CRT text-mode flag accepted by this target's open path. + /// + /// The Windows runtime normally chooses `_O_BINARY`; PHP's `t` fopen modifier + /// explicitly requests `_O_TEXT` and must override that default. + pub fn o_text(&self) -> Option { + match self { + Platform::Windows => Some(0x4000), + Platform::MacOS | Platform::Linux => None, } } @@ -82,7 +160,7 @@ impl Platform { match self { Platform::MacOS => 0x4048_7413, Platform::Linux => 0x5401, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 0x5401, } } @@ -92,7 +170,7 @@ impl Platform { match self { Platform::MacOS => 0x0004, Platform::Linux => 0x0800, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 0x0800, } } @@ -102,7 +180,7 @@ impl Platform { match self { Platform::MacOS => 0xffff, Platform::Linux => 1, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 1, } } @@ -112,7 +190,7 @@ impl Platform { match self { Platform::MacOS => 0x1006, Platform::Linux => 20, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 20, } } @@ -135,7 +213,7 @@ impl Platform { match self { Platform::MacOS => 0x0200, Platform::Linux => 15, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 15, } } @@ -146,7 +224,7 @@ impl Platform { match self { Platform::MacOS => 0x0020, Platform::Linux => 6, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 6, } } @@ -164,7 +242,7 @@ impl Platform { match self { Platform::MacOS => 27, Platform::Linux => 26, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 26, } } @@ -174,18 +252,22 @@ impl Platform { match self { Platform::MacOS => 61, Platform::Linux => 111, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 111, } } - /// `AF_INET6` family value — 30 on macOS (BSD), 10 on Linux. Passed to - /// `socket()` for IPv6 sockets and stored as the family byte in - /// `sockaddr_in6` before `bind()` / `connect()`. + /// `AF_INET6` family value — 30 on macOS (BSD), 10 on Linux, 23 on + /// Windows (`winsock2.h`). Passed to `socket()` for IPv6 sockets and + /// stored as the family byte in `sockaddr_in6` before `bind()` / + /// `connect()`, and passed as the `getaddrinfo`/`inet_pton`/`inet_ntop` + /// hint family on the Windows x86_64 helpers + /// (`__rt_resolve_host_v6`/`__rt_inet6_pton`/`__rt_format_sockaddr_in6`), + /// all of which read this accessor directly via `emitter.platform`. pub fn af_inet6(&self) -> i64 { match self { Platform::MacOS => 30, Platform::Linux => 10, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 23, } } @@ -194,12 +276,16 @@ impl Platform { /// — putting `ai_addr` at offset 32. Linux (glibc) swaps the canonname /// and addr fields, so `ai_addr` lives at offset 24. The earlier fields /// (ai_flags/family/socktype/protocol/addrlen + pad) are identical at - /// 24 bytes total on both LP64 platforms. + /// 24 bytes total on both LP64 platforms. Win64 `ADDRINFOA` + /// (`ws2def.h`) uses the BSD/macOS field order — `ai_addrlen` is an + /// 8-byte `size_t` and `ai_canonname` precedes `ai_addr` — so `ai_addr` + /// sits at offset 32 on Windows, NOT the glibc 24 (loading at 24 would + /// read `ai_canonname`, a `char*`, and dereference it as a `sockaddr`). pub fn addrinfo_addr_offset(&self) -> i64 { match self { Platform::MacOS => 32, Platform::Linux => 24, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 32, } } @@ -210,7 +296,7 @@ impl Platform { match self { Platform::MacOS => 0x201, Platform::Linux => 0x41, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 0x41, } } @@ -221,7 +307,7 @@ impl Platform { match self { Platform::MacOS => 0x209, Platform::Linux => 0x441, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 0x441, } } @@ -233,7 +319,7 @@ impl Platform { match self { Platform::MacOS => format!("b.cc {}", label), Platform::Linux => format!("b.ge {}", label), - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => format!("b.ge {}", label), } } @@ -242,7 +328,7 @@ impl Platform { /// Linux syscall results need a comparison against zero before branching on error, /// whereas macOS uses the condition flags set directly by `svc`. pub fn needs_cmp_before_error_branch(&self) -> bool { - matches!(self, Platform::Linux) + matches!(self, Platform::Linux | Platform::Windows) } /// Returns the platform errno value for `EAGAIN`/`EWOULDBLOCK`. @@ -253,7 +339,7 @@ impl Platform { match self { Platform::MacOS => 35, Platform::Linux => 11, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 11, } } @@ -264,7 +350,7 @@ impl Platform { match self { Platform::MacOS => 144, Platform::Linux => 128, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 128, } } @@ -273,7 +359,7 @@ impl Platform { match self { Platform::MacOS => 4, Platform::Linux => 16, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 16, } } @@ -285,7 +371,7 @@ impl Platform { match self { Platform::MacOS => format!("ldrh {}, [{}, #{}]", dest, base, offset), Platform::Linux => format!("ldr {}, [{}, #{}]", dest, base, offset), - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => format!("ldr {}, [{}, #{}]", dest, base, offset), } } @@ -294,7 +380,7 @@ impl Platform { match self { Platform::MacOS => 96, Platform::Linux => 48, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 48, } } @@ -303,7 +389,7 @@ impl Platform { match self { Platform::MacOS => 2168, Platform::Linux => 128, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 128, } } @@ -312,7 +398,7 @@ impl Platform { match self { Platform::MacOS => 0, Platform::Linux => 8, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 8, } } @@ -321,7 +407,7 @@ impl Platform { match self { Platform::MacOS => 8, Platform::Linux => 16, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 16, } } @@ -330,7 +416,7 @@ impl Platform { match self { Platform::MacOS => 24, Platform::Linux => 32, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 32, } } @@ -339,7 +425,7 @@ impl Platform { match self { Platform::MacOS => 48, Platform::Linux => 88, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 88, } } @@ -348,7 +434,7 @@ impl Platform { match self { Platform::MacOS => 32, Platform::Linux => 72, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 72, } } @@ -357,7 +443,7 @@ impl Platform { match self { Platform::MacOS => 64, Platform::Linux => 104, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 104, } } @@ -366,7 +452,7 @@ impl Platform { match self { Platform::MacOS => 8, Platform::Linux => 8, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 8, } } @@ -375,7 +461,7 @@ impl Platform { match self { Platform::MacOS => 16, Platform::Linux => 24, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 24, } } @@ -384,7 +470,7 @@ impl Platform { match self { Platform::MacOS => 20, Platform::Linux => 28, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 28, } } @@ -396,7 +482,7 @@ impl Platform { match self { Platform::MacOS => 0, Platform::Linux => 0, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 0, } } @@ -405,7 +491,7 @@ impl Platform { match self { Platform::MacOS => 24, Platform::Linux => 32, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 32, } } @@ -414,7 +500,7 @@ impl Platform { match self { Platform::MacOS => 6, Platform::Linux => 20, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 20, } } @@ -423,7 +509,7 @@ impl Platform { match self { Platform::MacOS => 112, Platform::Linux => 56, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 56, } } @@ -432,7 +518,7 @@ impl Platform { match self { Platform::MacOS => 104, Platform::Linux => 64, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 64, } } @@ -444,7 +530,7 @@ impl Platform { match self { Platform::MacOS => format!("ldrsw {}, [{}, #{}]", dest_x, base, offset), Platform::Linux => format!("ldr {}, [{}, #{}]", dest_x, base, offset), - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => format!("ldr {}, [{}, #{}]", dest_x, base, offset), } } @@ -455,7 +541,7 @@ impl Platform { match self { Platform::MacOS => format!("ldrsw {}, [{}, #{}]", dest_x, base, offset), Platform::Linux => format!("ldr {}, [{}, #{}]", dest_x, base, offset), - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => format!("ldr {}, [{}, #{}]", dest_x, base, offset), } } @@ -466,7 +552,7 @@ impl Platform { match self { Platform::MacOS => format!("ldrh {}, [{}, #{}]", dest_w, base, offset), Platform::Linux => format!("ldr {}, [{}, #{}]", dest_w, base, offset), - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => format!("ldr {}, [{}, #{}]", dest_w, base, offset), } } @@ -478,7 +564,7 @@ impl Platform { match self { Platform::MacOS => -2, Platform::Linux => -100, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => -100, } } @@ -490,7 +576,7 @@ impl Platform { match self { Platform::MacOS => -1, Platform::Linux => 0x3FFF_FFFF, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 0x3FFF_FFFF, } } @@ -499,7 +585,7 @@ impl Platform { match self { Platform::MacOS => 21, Platform::Linux => 19, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 19, } } @@ -508,7 +594,7 @@ impl Platform { match self { Platform::MacOS => 32, Platform::Linux => 8, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 8, } } @@ -522,12 +608,14 @@ impl Platform { 24 } - /// Returns the value of `LC_CTYPE` for `setlocale()`. + /// Returns the value of `LC_CTYPE` for `setlocale()`. msvcrt's + /// `` numbers categories `LC_ALL=0, LC_COLLATE=1, LC_CTYPE=2, + /// ...` — same as macOS/BSD (`LC_CTYPE=2`) — NOT glibc's `LC_CTYPE=0`. pub fn lc_ctype(&self) -> u32 { match self { Platform::MacOS => 2, Platform::Linux => 0, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 2, } } @@ -582,7 +670,10 @@ impl Target { /// /// Supported values: `macos-aarch64`, `macos-arm64`, `aarch64-apple-darwin`, /// `macos-x86_64`, `x86_64-apple-darwin`, `linux-aarch64`, `linux-arm64`, - /// `aarch64-unknown-linux-gnu`, `linux-x86_64`, `x86_64-unknown-linux-gnu`. + /// `aarch64-unknown-linux-gnu`, `linux-x86_64`, `x86_64-unknown-linux-gnu`, + /// `windows-x86_64`, `x86_64-pc-windows-gnu`, `x86_64-pc-windows-msvc`. + /// Both Windows triples select elephc's MinGW GNU-ABI/mingw-w64 backend; the + /// MSVC spelling is accepted as an input alias and does not select MSVC linkage. /// Returns an error for any unrecognized string. pub fn parse(value: &str) -> Result { match value { @@ -617,8 +708,8 @@ impl Target { (Platform::MacOS, Arch::X86_64) => "macos-x86_64", (Platform::Linux, Arch::AArch64) => "linux-aarch64", (Platform::Linux, Arch::X86_64) => "linux-x86_64", - (Platform::Windows, Arch::AArch64) => "windows-aarch64", (Platform::Windows, Arch::X86_64) => "windows-x86_64", + (Platform::Windows, Arch::AArch64) => "windows-aarch64", } } @@ -631,6 +722,7 @@ impl Target { (Platform::MacOS, Arch::AArch64) | (Platform::Linux, Arch::AArch64) | (Platform::Linux, Arch::X86_64) + | (Platform::Windows, Arch::X86_64) ) } @@ -666,6 +758,7 @@ impl Target { match (self.platform, self.arch) { (Platform::MacOS, Arch::AArch64) => asm.to_string(), (Platform::Linux, Arch::AArch64) => transform_for_linux(asm), + (Platform::Windows, Arch::X86_64) => transform_for_windows(asm), _ => asm.to_string(), } } @@ -677,7 +770,7 @@ impl Target { match (self.platform, self.arch) { (Platform::MacOS, Arch::AArch64) => ";", (Platform::Linux, Arch::AArch64) => "//", - (Platform::Windows, _) => ";", + (Platform::Windows, Arch::AArch64) => "//", (_, Arch::X86_64) => "#", } } @@ -764,6 +857,7 @@ impl Target { /// /// On macOS always uses `as`. On Linux ARM64 uses `as` if a native toolchain /// is available, otherwise `aarch64-linux-gnu-as`. On Linux x86_64 uses `as`. + /// On Windows x86_64 uses `x86_64-w64-mingw32-as` (MinGW GAS). pub fn assembler_cmd(&self) -> &'static str { match (self.platform, self.arch) { (Platform::MacOS, Arch::AArch64 | Arch::X86_64) => "as", @@ -775,7 +869,10 @@ impl Target { } } (Platform::Linux, Arch::X86_64) => "as", - (Platform::Windows, _) => "as", + (Platform::Windows, Arch::X86_64) => "x86_64-w64-mingw32-as", + (Platform::Windows, Arch::AArch64) => { + panic!("Windows ARM64 target is not yet supported (see issue #379)") + } } } @@ -783,6 +880,7 @@ impl Target { /// /// On macOS always uses `ld`. On Linux ARM64 uses `gcc` if a native toolchain /// is available, otherwise `aarch64-linux-gnu-gcc`. On Linux x86_64 uses `gcc`. + /// On Windows x86_64 uses `x86_64-w64-mingw32-gcc` (MinGW GCC). pub fn linker_cmd(&self) -> &'static str { match (self.platform, self.arch) { (Platform::MacOS, Arch::AArch64 | Arch::X86_64) => "ld", @@ -794,7 +892,10 @@ impl Target { } } (Platform::Linux, Arch::X86_64) => "gcc", - (Platform::Windows, _) => "gcc", + (Platform::Windows, Arch::X86_64) => "x86_64-w64-mingw32-gcc", + (Platform::Windows, Arch::AArch64) => { + panic!("Windows ARM64 target is not yet supported (see issue #379)") + } } } } diff --git a/src/codegen_support/platform/windows_transform.rs b/src/codegen_support/platform/windows_transform.rs new file mode 100644 index 0000000000..160718f158 --- /dev/null +++ b/src/codegen_support/platform/windows_transform.rs @@ -0,0 +1,1229 @@ +//! Purpose: +//! Transforms Linux x86_64 assembly into Windows-compatible assembly by replacing +//! raw `syscall` sequences with calls to Win32 shim wrappers. This is the central +//! mechanism that lets the existing x86_64 runtime code emit SysV-convention argument +//! setup while running on Windows — the shims convert SysV registers to MSx64 ABI +//! before calling Win32 API functions. +//! +//! Called from: +//! - `crate::pipeline::compile()` on the emitted user assembly (Windows target only). +//! - `crate::runtime_cache::prepare_runtime_object()` on the runtime assembly +//! before hashing/assembling (Windows target only). +//! - `crate::codegen::platform::target::Target::transform_assembly()` (test harness). +//! +//! Key details: +//! - Each `mov eax, \n syscall` pair is replaced by `call __rt_sys_`. +//! - The syscall number N is the Linux x86_64 syscall number. +//! - Unsupported syscall numbers are rewritten to keep the number in `eax` and +//! `call __rt_unsupported_syscall`, which prints `unsupported syscall: ` to +//! stderr and exits — instead of a silent `int3` that would just crash. +//! - Runtime-internal calls (`call __rt_*`) are left unchanged — they are not syscalls. + +/// Maps a Linux x86_64 syscall number to its Win32 shim function name. +/// +/// Returns `None` for syscalls that do not yet have a Win32 equivalent. +/// Unsupported syscalls are routed to `__rt_unsupported_syscall`, which reports +/// the number on stderr and exits (see `transform_for_windows`). +fn linux_syscall_to_shim(linux_num: u32) -> Option<&'static str> { + match linux_num { + 0 => Some("__rt_sys_read"), + 1 => Some("__rt_sys_write"), + 2 => Some("__rt_sys_open"), + 3 => Some("__rt_sys_close"), + 8 => Some("__rt_sys_lseek"), + 9 => Some("__rt_sys_mmap"), + 10 => Some("__rt_sys_mprotect"), + 11 => Some("__rt_sys_munmap"), + 12 => Some("__rt_sys_brk"), + 16 => Some("__rt_sys_ioctl"), + 20 => Some("__rt_sys_writev"), + 21 => Some("__rt_sys_access"), + 28 => Some("__rt_sys_accept4"), + 32 => Some("__rt_sys_dup2"), + 33 => Some("__rt_sys_dup"), + 41 => Some("__rt_sys_socket"), + 42 => Some("__rt_sys_connect"), + 43 => Some("__rt_sys_accept"), + 44 => Some("__rt_sys_sendto"), + 45 => Some("__rt_sys_recvfrom"), + 46 => Some("__rt_sys_sendmsg"), + 47 => Some("__rt_sys_recvmsg"), + 48 => Some("__rt_sys_shutdown"), + 49 => Some("__rt_sys_bind"), + 50 => Some("__rt_sys_listen"), + 51 => Some("__rt_sys_getsockname"), + 52 => Some("__rt_sys_getpeername"), + 53 => Some("__rt_sys_socketpair"), + 54 => Some("__rt_sys_setsockopt"), + 55 => Some("__rt_sys_getsockopt"), + 59 => Some("__rt_sys_execve"), + 60 => Some("__rt_sys_exit"), + 62 => Some("__rt_sys_kill"), + 63 => Some("__rt_sys_uname"), + 72 => Some("__rt_sys_fcntl"), + 77 => Some("__rt_sys_ftruncate"), + 78 => Some("__rt_sys_getdents"), + 79 => Some("__rt_sys_getcwd"), + 80 => Some("__rt_sys_chdir"), + 82 => Some("__rt_sys_rename"), + 83 => Some("__rt_sys_mkdir"), + 84 => Some("__rt_sys_rmdir"), + 85 => Some("__rt_sys_creat"), + 86 => Some("__rt_sys_link"), + 87 => Some("__rt_sys_unlink"), + 88 => Some("__rt_sys_symlink"), + 89 => Some("__rt_sys_readlink"), + 90 => Some("__rt_sys_chmod"), + 92 => Some("__rt_sys_chown"), + 93 => Some("__rt_sys_fchown"), + 94 => Some("__rt_sys_lchown"), + 96 => Some("__rt_sys_getpriority"), + 97 => Some("__rt_sys_setpriority"), + 98 => Some("__rt_sys_getrusage"), + 102 => Some("__rt_sys_getuid"), + 104 => Some("__rt_sys_getgid"), + 105 => Some("__rt_sys_setuid"), + 106 => Some("__rt_sys_setgid"), + 110 => Some("__rt_sys_getppid"), + 116 => Some("__rt_sys_sysinfo"), + 137 => Some("__rt_sys_statfs"), + 160 => Some("__rt_sys_uname"), + 202 => Some("__rt_sys_futex"), + 228 => Some("__rt_sys_clock_gettime"), + 230 => Some("__rt_sys_clock_getres"), + 262 => Some("__rt_sys_newfstatat"), + 270 => Some("__rt_sys_pselect6"), + 318 => Some("__rt_sys_getrandom"), + _ => None, + } +} + +/// Transforms Linux x86_64 assembly to Windows-compatible assembly. +/// +/// Replaces each `mov eax, ` followed by `syscall` with `call __rt_sys_`. +/// The argument setup (in SysV registers rdi/rsi/rdx/r10/rcx/r8/r9) is left intact — +/// the Win32 shim wrappers read arguments from SysV registers and convert them to +/// MSx64 ABI before calling the corresponding Win32 API function. +/// +/// Unsupported syscall numbers are rewritten to keep the number in `eax` and +/// `call __rt_unsupported_syscall`, the runtime helper that prints +/// `unsupported syscall: ` to stderr and exits — a visible diagnostic instead +/// of a silent `int3` crash. +pub fn transform_for_windows(asm: &str) -> String { + let lines: Vec<&str> = asm.lines().collect(); + let mut out = String::with_capacity(asm.len() + 256); + let mut i = 0; + while i < lines.len() { + let line = lines[i]; + let trimmed = line.trim(); + if let Some(num) = parse_linux_syscall(trimmed) { + if i + 1 < lines.len() && is_syscall_line(lines[i + 1]) { + match linux_syscall_to_shim(num) { + Some(shim) => { + out.push_str(" call "); + out.push_str(shim); + out.push('\n'); + } + None => { + // No Win32 shim for this syscall: keep the number in eax + // and route to the runtime diagnostic helper, which prints + // "unsupported syscall: " to stderr and exits — instead + // of a silent int3 that would just crash. + out.push_str(" mov eax, "); + out.push_str(&num.to_string()); + out.push('\n'); + out.push_str(" call __rt_unsupported_syscall\n"); + } + } + i += 2; + continue; + } + } + out.push_str(line); + out.push('\n'); + i += 1; + } + let promoted = promote_independent_frame_regions(&out); + add_safe_frame_unwind_metadata(&promoted) +} + +/// Promotes independent internal frame starts to synthetic COFF-local function symbols. +/// +/// Runtime emitters often place a frameless fast path or a completed helper before +/// an internal slow-path label that establishes its own RBP frame. PE requires a +/// distinct Function Table range for that later prologue. This pass inserts a +/// synthetic `.L` label only when the preceding linear region has balanced every +/// RBP push, so a still-active outer frame is never split accidentally. GNU as +/// discards `.L` symbols from the COFF symbol table, preventing runtime and user +/// objects from exporting colliding synthetic names. +fn promote_independent_frame_regions(asm: &str) -> String { + let lines = asm.lines().collect::>(); + let mut out = String::with_capacity(asm.len() + 512); + let mut rbp_depth = 0usize; + let mut synthetic_index = 0usize; + + for (index, line) in lines.iter().enumerate() { + let trimmed = line.trim(); + if trimmed.starts_with(".globl ") { + rbp_depth = 0; + } + let is_canonical_start = trimmed == "push rbp" + && lines.get(index + 1).is_some_and(|line| line.trim() == "mov rbp, rsp") + && lines + .get(index + 2) + .is_some_and(|line| line.trim().starts_with("sub rsp, ")); + if is_canonical_start && rbp_depth == 0 && !global_label_immediately_precedes(&lines, index) { + let symbol = format!(".L__elephc_unwind_region_{synthetic_index}"); + out.push_str(&symbol); + out.push_str(":\n"); + synthetic_index += 1; + } + out.push_str(line); + out.push('\n'); + if trimmed == "push rbp" { + rbp_depth += 1; + } else if matches!(trimmed, "pop rbp" | "leave") { + rbp_depth = rbp_depth.saturating_sub(1); + } + } + out +} + +/// Returns whether a global symbol and its label directly own this prologue. +fn global_label_immediately_precedes(lines: &[&str], push_index: usize) -> bool { + let significant = lines[..push_index] + .iter() + .rev() + .filter(|line| { + let line = line.trim(); + !line.is_empty() && !line.starts_with('#') + }) + .take(2) + .map(|line| line.trim()) + .collect::>(); + significant.len() == 2 + && significant[0].ends_with(':') + && significant[1].starts_with(".globl ") + && significant[1].trim_start_matches(".globl ").trim() + == significant[0].trim_end_matches(':') +} + +/// Adds PE/COFF unwind directives around exactly recognized x86_64 frames. +/// +/// EIR codegen brackets its function-like regions with `@fn`/`@endfn` markers. +/// Within those regions every global body using the standard x86_64 frame +/// prologue gets a `.seh_proc` record describing the saved frame pointer, fixed +/// stack allocation, and register-allocator callee saves. Hand-written runtime +/// helpers are accepted only when their entire body passes the stricter runtime +/// audit: no unrepresented stack-pointer mutation, push/pop, ambiguous return, +/// or live-frame tail call is allowed. +fn add_safe_frame_unwind_metadata(asm: &str) -> String { + let lines = asm.lines().collect::>(); + let mut out = String::with_capacity(asm.len() + 512); + let mut in_eir_region = false; + let mut index = 0; + + while index < lines.len() { + let trimmed = lines[index].trim(); + if trimmed.starts_with("# @fn ") { + in_eir_region = true; + } + if let Some(symbol) = frame_region_symbol(trimmed) { + let segment_end = function_segment_end(&lines, index); + let segment = &lines[index..segment_end]; + let metadata = standard_frame_metadata(segment, symbol); + let is_generated = in_eir_region; + let is_safe_runtime = metadata + .as_ref() + .is_some_and(|metadata| audit_runtime_frame(segment, metadata).is_ok()); + if (is_generated || is_safe_runtime) && metadata.is_some() { + let metadata = metadata.expect("frame metadata checked above"); + out.push_str(".seh_proc "); + out.push_str(symbol); + out.push('\n'); + for (relative, line) in lines[index..segment_end].iter().enumerate() { + out.push_str(line); + out.push('\n'); + if relative == metadata.push_index { + out.push_str(" .seh_pushreg rbp\n"); + } + if relative == metadata.stack_alloc_index { + out.push_str(" .seh_stackalloc "); + out.push_str(&metadata.stack_bytes.to_string()); + out.push('\n'); + } + if metadata + .frame_pointer + .is_some_and(|frame_pointer| frame_pointer.line_index == relative) + { + let frame_pointer = metadata.frame_pointer.expect("frame pointer checked"); + out.push_str(" .seh_setframe rbp, "); + out.push_str(&frame_pointer.rsp_offset.to_string()); + out.push('\n'); + } + for save in metadata.saves.iter().filter(|save| save.line_index == relative) { + out.push_str(" .seh_savereg "); + out.push_str(save.register); + out.push_str(", "); + out.push_str(&save.rsp_offset.to_string()); + out.push('\n'); + } + if relative == metadata.prologue_end_index { + out.push_str(" .seh_endprologue\n"); + } + } + out.push_str(".seh_endproc\n"); + index = segment_end; + continue; + } + } + out.push_str(lines[index]); + out.push('\n'); + if trimmed.starts_with("# @endfn ") { + in_eir_region = false; + } + index += 1; + } + out +} + +/// Finds the end of one global text symbol without absorbing a following data section. +fn function_segment_end(lines: &[&str], start: usize) -> usize { + lines[start + 1..] + .iter() + .position(|line| { + let line = line.trim(); + frame_region_symbol(line).is_some() + || line.starts_with("# @endfn ") + || matches!(line, ".data" | ".bss" | ".text") + || line.starts_with(".section ") + }) + .map_or(lines.len(), |offset| start + 1 + offset) +} + +/// Returns the function symbol introduced by a global directive or synthetic local label. +fn frame_region_symbol(line: &str) -> Option<&str> { + line.strip_prefix(".globl ") + .map(str::trim) + .or_else(|| { + line.strip_suffix(':') + .filter(|symbol| symbol.starts_with(".L__elephc_unwind_region_")) + }) +} + +/// Returns whether a hand-written runtime frame is fully described by `metadata`. +/// +/// This deliberately recognizes a small subset. A rejected helper simply remains +/// without PE unwind data; accepting an ambiguous body would be worse because the +/// OS could restore a fabricated stack or register state during `RtlUnwindEx`. +fn audit_runtime_frame( + lines: &[&str], + metadata: &GeneratedFrameMetadata<'_>, +) -> Result<(), RuntimeFrameRejection> { + let mut saw_return = false; + let mut saw_safe_tail_exit = false; + let mut saw_terminal_trap = false; + let local_labels = lines + .iter() + .filter_map(|line| line.trim().strip_suffix(':')) + .collect::>(); + + for (index, raw) in lines.iter().enumerate().skip(metadata.prologue_end_index + 1) { + let line = raw.trim(); + if line.is_empty() || line.starts_with('#') || line.starts_with('.') || line.ends_with(':') { + continue; + } + + if line.starts_with("push ") || (line.starts_with("pop ") && line != "pop rbp") { + return Err(RuntimeFrameRejection::AdditionalPushOrPop); + } + if writes_stack_or_frame_pointer(line) { + let is_epilogue = matches!(line, "mov rsp, rbp" | "pop rbp" | "leave") + || is_fixed_frame_deallocation(lines, index, metadata.stack_bytes); + let is_dynamic_rsp_with_frame_pointer = metadata.has_encodable_frame_pointer() + && first_operand(line).is_some_and(|operand| { + matches!(operand, "rsp" | "esp" | "sp" | "spl") + }); + if !is_epilogue && !is_dynamic_rsp_with_frame_pointer { + return Err(RuntimeFrameRejection::UnencodableStackMutation); + } + } + + if line == "ret" { + saw_return = true; + let Some(previous) = previous_instruction(lines, index) else { + return Err(RuntimeFrameRejection::AmbiguousReturn); + }; + if previous != "leave" + && previous != "pop rbp" + && !metadata.has_encodable_frame_pointer() + { + return Err(RuntimeFrameRejection::AmbiguousReturn); + } + } + if line == "ud2" { + saw_terminal_trap = true; + } + + if let Some(target) = line.strip_prefix("jmp ").map(str::trim) { + if !local_labels.contains(target) { + if !basic_block_released_frame(lines, index) + && !is_audited_runtime_tail(metadata, target) + { + return Err(RuntimeFrameRejection::LiveFrameTailCall); + } + saw_safe_tail_exit = true; + } + } + } + + if saw_return || saw_safe_tail_exit || saw_terminal_trap { + Ok(()) + } else { + Err(RuntimeFrameRejection::NoReturn) + } +} + +/// Recognizes runtime tail edges whose destination frame contract was audited explicitly. +fn is_audited_runtime_tail( + metadata: &GeneratedFrameMetadata<'_>, + target: &str, +) -> bool { + matches!( + (metadata.symbol, target, metadata.stack_bytes), + ( + "__rt_mixed_numeric_add" | "__rt_mixed_numeric_sub", + "__rt_mixed_numeric_common_linux_x86_64", + 80 + ) | ("__rt_opendir", "__rt_opendir_native_x86", 16) + ) +} + +/// Returns whether the current basic block tore down its RBP frame before a tail exit. +fn basic_block_released_frame(lines: &[&str], before: usize) -> bool { + lines[..before] + .iter() + .rev() + .take_while(|line| !line.trim().ends_with(':')) + .any(|line| matches!(line.trim(), "pop rbp" | "leave")) +} + +/// Reason a canonical runtime frame cannot truthfully receive one unwind record. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +enum RuntimeFrameRejection { + AdditionalPushOrPop, + UnencodableStackMutation, + AmbiguousReturn, + LiveFrameTailCall, + NoReturn, +} + +/// Returns whether an `add rsp, frame_size` starts a canonical pop/return epilogue. +fn is_fixed_frame_deallocation( + lines: &[&str], + index: usize, + stack_bytes: usize, +) -> bool { + if lines[index].trim() != format!("add rsp, {stack_bytes}") { + return false; + } + let Some((pop_index, pop)) = next_instruction(lines, index) else { + return false; + }; + if pop != "pop rbp" { + return false; + } + next_instruction(lines, pop_index).is_some_and(|(_, instruction)| { + instruction == "ret" || instruction.starts_with("jmp ") + }) +} + +/// Returns the next non-comment, non-directive assembly instruction and its index. +fn next_instruction<'a>(lines: &[&'a str], after: usize) -> Option<(usize, &'a str)> { + lines.iter().enumerate().skip(after + 1).find_map(|(index, line)| { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') || line.starts_with('.') || line.ends_with(':') { + None + } else { + Some((index, line)) + } + }) +} + +/// Returns the previous non-comment, non-directive assembly instruction. +fn previous_instruction<'a>(lines: &[&'a str], before: usize) -> Option<&'a str> { + lines[..before].iter().rev().find_map(|line| { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') || line.starts_with('.') || line.ends_with(':') { + None + } else { + Some(line) + } + }) +} + +/// Detects instructions that directly change `rsp` or `rbp`. +fn writes_stack_or_frame_pointer(line: &str) -> bool { + let Some((mnemonic, operands)) = line.split_once(char::is_whitespace) else { + return line == "leave"; + }; + if !instruction_writes_first_operand(mnemonic) { + return false; + } + let destination = operands.split(',').next().unwrap_or(operands).trim(); + matches!(destination, "rsp" | "esp" | "sp" | "spl" | "rbp" | "ebp" | "bp" | "bpl") +} + +/// Returns the first Intel-syntax operand of an instruction. +fn first_operand(line: &str) -> Option<&str> { + let (_, operands) = line.split_once(char::is_whitespace)?; + Some(operands.split(',').next().unwrap_or(operands).trim()) +} + +/// Returns whether a runtime segment is a true MS x64 leaf needing no unwind table. +#[cfg(test)] +fn is_leaf_without_unwind_state(lines: &[&str]) -> bool { + lines.iter().all(|raw| { + let line = raw.trim(); + if line.is_empty() || line.starts_with('#') || line.starts_with('.') || line.ends_with(':') { + return true; + } + !line.starts_with("call ") + && !line.starts_with("push ") + && !line.starts_with("pop ") + && !writes_stack_or_frame_pointer(line) + && !writes_windows_nonvolatile_register(line) + }) +} + +/// Detects an explicit or string-instruction write to an MS x64 nonvolatile register. +#[cfg(test)] +fn writes_windows_nonvolatile_register(line: &str) -> bool { + if line.starts_with("rep ") + || line.starts_with("movs") + || line.starts_with("stos") + || line.starts_with("lods") + || line.starts_with("scas") + { + return true; + } + let Some((mnemonic, operands)) = line.split_once(char::is_whitespace) else { + return false; + }; + if mnemonic == "xchg" { + return operands + .split(',') + .any(|operand| is_windows_nonvolatile_register(operand.trim())); + } + instruction_writes_first_operand(mnemonic) + && first_operand(line).is_some_and(is_windows_nonvolatile_register) +} + +/// Returns whether an exact operand names an MS x64 nonvolatile register. +#[cfg(test)] +fn is_windows_nonvolatile_register(operand: &str) -> bool { + matches!( + operand, + "rbx" | "ebx" | "bx" | "bl" + | "rsi" | "esi" | "si" | "sil" + | "rdi" | "edi" | "di" | "dil" + | "r12" | "r12d" | "r12w" | "r12b" + | "r13" | "r13d" | "r13w" | "r13b" + | "r14" | "r14d" | "r14w" | "r14b" + | "r15" | "r15d" | "r15w" | "r15b" + | "xmm6" | "xmm7" | "xmm8" | "xmm9" | "xmm10" | "xmm11" + | "xmm12" | "xmm13" | "xmm14" | "xmm15" + ) +} + +/// Returns whether a mnemonic writes its first Intel-syntax operand. +fn instruction_writes_first_operand(mnemonic: &str) -> bool { + matches!( + mnemonic, + "mov" + | "movabs" + | "movsx" + | "movsxd" + | "movzx" + | "lea" + | "add" + | "adc" + | "sub" + | "sbb" + | "imul" + | "and" + | "or" + | "xor" + | "not" + | "neg" + | "inc" + | "dec" + | "shl" + | "shr" + | "sar" + | "rol" + | "ror" + | "pop" + | "cvtsi2sd" + | "cvttsd2si" + | "movq" + | "movd" + | "movsd" + | "movss" + | "movaps" + | "movups" + | "movdqa" + | "movdqu" + ) || mnemonic.starts_with("cmov") + || mnemonic.starts_with("set") +} + + +/// Unwind-relevant indices and offsets for one standard generated frame. +struct GeneratedFrameMetadata<'a> { + symbol: &'a str, + push_index: usize, + stack_alloc_index: usize, + stack_bytes: usize, + frame_pointer: Option, + saves: Vec>, + prologue_end_index: usize, +} + +impl GeneratedFrameMetadata<'_> { + /// Returns whether PE can use `rbp` to recover the fixed frame across dynamic RSP changes. + fn has_encodable_frame_pointer(&self) -> bool { + self.frame_pointer.is_some() + } +} + +/// One PE-encodable RBP frame-pointer declaration and its instruction position. +#[derive(Clone, Copy)] +struct GeneratedFramePointer { + line_index: usize, + rsp_offset: usize, +} + +/// One nonvolatile register save expressed relative to the post-prologue RSP. +struct GeneratedRegisterSave<'a> { + line_index: usize, + register: &'a str, + rsp_offset: usize, +} + +/// Recognizes the standard generated frame prologue and derives safe SEH codes. +fn standard_frame_metadata<'a>( + lines: &[&'a str], + symbol: &'a str, +) -> Option> { + let label_index = lines.iter().position(|line| line.trim() == format!("{symbol}:"))?; + let push_index = next_generated_prologue_instruction(lines, label_index)?; + if lines[push_index].trim() != "push rbp" { + return None; + } + let frame_pointer_index = next_generated_prologue_instruction(lines, push_index)?; + if lines[frame_pointer_index].trim() != "mov rbp, rsp" { + return None; + } + let stack_alloc_index = next_generated_prologue_instruction(lines, frame_pointer_index)?; + if !lines[stack_alloc_index].trim().starts_with("sub rsp, ") { + return None; + } + let stack_bytes = lines[stack_alloc_index] + .trim() + .trim_start_matches("sub rsp, ") + .parse::() + .ok()?; + let frame_pointer = if stack_bytes <= 240 && stack_bytes % 16 == 0 { + Some(GeneratedFramePointer { + line_index: stack_alloc_index, + rsp_offset: stack_bytes, + }) + } else { + next_generated_prologue_instruction(lines, stack_alloc_index).and_then(|adjustment_index| { + let adjustment = lines[adjustment_index] + .trim() + .strip_prefix("sub rbp, ")? + .parse::() + .ok()?; + let rsp_offset = stack_bytes.checked_sub(adjustment)?; + (rsp_offset <= 240 && rsp_offset % 16 == 0).then_some(GeneratedFramePointer { + line_index: adjustment_index, + rsp_offset, + }) + }) + }; + let save_marker = lines[stack_alloc_index + 1..] + .iter() + .position(|line| line.trim() == "# save callee-saved registers used by the register allocator") + .map(|offset| stack_alloc_index + 1 + offset); + let mut saves = Vec::new(); + if let Some(marker) = save_marker { + for (line_index, line) in lines.iter().enumerate().skip(marker + 1) { + if line.trim().starts_with("movsd QWORD PTR [rbp - ") { + // The allocator currently has no callee-saved XMM pool on + // x86_64. Refuse to annotate a future scalar-only XMM save: + // Windows unwind metadata restores the full 128-bit register, + // which would not match `movsd`'s 64-bit frame write. + return None; + } + let Some((register, frame_offset)) = parse_generated_register_save(line) else { + break; + }; + if !matches!(register, "rbx" | "rsi" | "rdi" | "r12" | "r13" | "r14" | "r15") { + return None; + } + let rsp_offset = stack_bytes.checked_sub(frame_offset)?; + if rsp_offset % 8 != 0 { + return None; + } + saves.push(GeneratedRegisterSave { + line_index, + register, + rsp_offset, + }); + } + } + let prologue_end_index = saves + .last() + .map_or(stack_alloc_index, |save| save.line_index) + .max(frame_pointer.map_or(stack_alloc_index, |frame_pointer| frame_pointer.line_index)); + Some(GeneratedFrameMetadata { + symbol, + push_index, + stack_alloc_index, + stack_bytes, + frame_pointer, + saves, + prologue_end_index, + }) +} + +/// Finds the next real instruction in a generated prologue, skipping only +/// blank lines and emitter comments; encountering a directive is not ignored. +fn next_generated_prologue_instruction(lines: &[&str], after: usize) -> Option { + lines.iter().enumerate().skip(after + 1).find_map(|(index, line)| { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + None + } else { + Some(index) + } + }) +} + +/// Parses a register-allocator save of the form `[rbp - offset], register`. +fn parse_generated_register_save(line: &str) -> Option<(&str, usize)> { + let line = line.trim(); + let line = line.strip_prefix("mov QWORD PTR [rbp - ")?; + let (offset, register) = line.split_once("], ")?; + Some((register.trim(), offset.parse().ok()?)) +} + +/// Extracts the syscall number from a `mov eax, ` instruction line. +/// Returns `Some(N)` if the line is a syscall number load, `None` otherwise. +fn parse_linux_syscall(line: &str) -> Option { + let line = line.trim(); + if !line.starts_with("mov eax, ") { + return None; + } + let rest = &line["mov eax, ".len()..]; + if rest.starts_with('#') || rest.starts_with("//") { + return None; + } + let num_part = rest.split_whitespace().next()?; + num_part.parse::().ok() +} + +/// Returns `true` if the line is a standalone `syscall` instruction. +/// +/// Tolerates leading/trailing whitespace and an optional trailing assembler +/// comment (`#` for the x86_64 emitter) so a `syscall` immediately following a +/// `mov eax, ` is still recognized even if a comment is ever appended. A +/// longer mnemonic that merely starts with `syscall` (e.g. `syscallfoo`) is not +/// matched. +fn is_syscall_line(line: &str) -> bool { + let trimmed = line.trim(); + match trimmed.strip_prefix("syscall") { + Some("") => true, + Some(rest) => rest.starts_with(char::is_whitespace) || rest.starts_with('#'), + None => false, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Verifies that a `mov eax, 1; syscall` sequence is replaced by `call __rt_sys_write`. + #[test] + fn test_write_syscall_replacement() { + let input = " mov eax, 1\n syscall\n"; + let output = transform_for_windows(input); + assert!(output.contains("call __rt_sys_write")); + assert!(!output.contains("syscall")); + } + + /// Verifies that `mov eax, 60; syscall` (exit) is replaced by `call __rt_sys_exit`. + #[test] + fn test_exit_syscall_replacement() { + let input = " mov eax, 60\n syscall\n"; + let output = transform_for_windows(input); + assert!(output.contains("call __rt_sys_exit")); + assert!(!output.contains("syscall")); + } + + /// Verifies that an unsupported syscall number is routed to the runtime + /// diagnostic helper (`call __rt_unsupported_syscall` with the number kept in + /// eax) rather than a silent `int3`. + #[test] + fn test_unsupported_syscall_produces_diagnostic_call() { + let input = " mov eax, 999\n syscall\n"; + let output = transform_for_windows(input); + assert!(output.contains("mov eax, 999")); + assert!(output.contains("call __rt_unsupported_syscall")); + assert!(!output.contains("int3")); + // The raw ` syscall` instruction line must be gone (the helper name + // ending in "syscall" is a call operand, not a standalone instruction). + assert!(!output.lines().any(|l| l.trim() == "syscall")); + } + + /// Verifies that non-syscall `mov eax` lines are preserved. + #[test] + fn test_non_syscall_mov_eax_preserved() { + let input = " mov eax, 1\n ret\n"; + let output = transform_for_windows(input); + assert!(output.contains("mov eax, 1")); + assert!(output.contains("ret")); + } + + /// Verifies that syscall with comment after the number is still parsed. + #[test] + fn test_syscall_with_comment() { + let input = " mov eax, 1\n syscall\n"; + let output = transform_for_windows(input); + assert!(output.contains("call __rt_sys_write")); + } + + /// Verifies the full mapping of common syscalls. + #[test] + fn test_linux_syscall_to_shim_mapping() { + assert_eq!(linux_syscall_to_shim(0), Some("__rt_sys_read")); + assert_eq!(linux_syscall_to_shim(1), Some("__rt_sys_write")); + assert_eq!(linux_syscall_to_shim(3), Some("__rt_sys_close")); + assert_eq!(linux_syscall_to_shim(9), Some("__rt_sys_mmap")); + assert_eq!(linux_syscall_to_shim(21), Some("__rt_sys_access")); + assert_eq!(linux_syscall_to_shim(60), Some("__rt_sys_exit")); + assert_eq!(linux_syscall_to_shim(77), Some("__rt_sys_ftruncate")); + assert_eq!(linux_syscall_to_shim(82), Some("__rt_sys_rename")); + assert_eq!(linux_syscall_to_shim(98), Some("__rt_sys_getrusage")); + assert_eq!(linux_syscall_to_shim(999), None); + } + + /// Verifies that munmap (syscall 11) maps to its VirtualFree shim so it never + /// degrades to `int3` if an x86_64 code path ever emits it inline. + #[test] + fn test_munmap_syscall_mapped() { + assert_eq!(linux_syscall_to_shim(11), Some("__rt_sys_munmap")); + } + + /// Verifies that a `syscall` line carrying a trailing assembler comment is + /// still recognized and rewritten (hardening for comment-annotated output). + #[test] + fn test_syscall_with_trailing_comment_is_transformed() { + let input = " mov eax, 1\n syscall # write bytes\n"; + let output = transform_for_windows(input); + assert!(output.contains("call __rt_sys_write")); + assert!(!contains_standalone_syscall(&output)); + } + + /// Verifies the `is_syscall_line` predicate: bare, comment-suffixed, and + /// whitespace-suffixed `syscall` match; a longer mnemonic does not. + #[test] + fn test_is_syscall_line_predicate() { + assert!(is_syscall_line(" syscall")); + assert!(is_syscall_line("syscall")); + assert!(is_syscall_line(" syscall # comment")); + assert!(is_syscall_line(" syscall\t")); + assert!(!is_syscall_line(" syscallfoo")); + assert!(!is_syscall_line(" mov eax, 1")); + } + + /// Verifies standard EIR frames receive PE unwind directives while their + /// existing instruction stream stays intact. + #[test] + fn test_generated_eir_frame_receives_unwind_metadata() { + let input = r#" # @fn name=demo symbol=_fn_demo +.globl _fn_demo +_fn_demo: + # prologue + push rbp + mov rbp, rsp + sub rsp, 64 + ret + # @endfn name=demo +"#; + let output = transform_for_windows(input); + assert!(output.contains(".seh_proc _fn_demo")); + assert!(output.contains("push rbp\n .seh_pushreg rbp")); + assert!(output.contains( + "sub rsp, 64\n .seh_stackalloc 64\n .seh_setframe rbp, 64\n .seh_endprologue" + )); + assert!(output.contains(".seh_endproc\n # @endfn name=demo")); + } + + /// Verifies a generated frame after a frameless fiber guard gets its own PE range. + #[test] + fn test_generated_eir_frame_after_fiber_guard_uses_local_unwind_region() { + let input = r#" # @fn name=demo symbol=_fn_demo +.globl _fn_demo +_fn_demo: + mov r11, QWORD PTR [rip + _fiber_current] + test r11, r11 + jz 8f + call __rt_win_fiber_stack_overflow_abort + ud2 +8: + push rbp + mov rbp, rsp + sub rsp, 64 + add rsp, 64 + pop rbp + ret + # @endfn name=demo +"#; + let output = transform_for_windows(input); + assert!(output.contains(".seh_proc .L__elephc_unwind_region_0")); + assert!(output.contains(".L__elephc_unwind_region_0:\n push rbp")); + assert!(!output.contains(".seh_proc _fn_demo")); + assert!(output.contains("push rbp\n .seh_pushreg rbp")); + assert!(output.contains(".seh_stackalloc 64")); + assert!(output.contains(".seh_endproc\n # @endfn name=demo")); + } + + /// Verifies integer register-allocator saves are represented relative to + /// the final stack pointer so Windows unwinding restores them exactly. + #[test] + fn test_generated_eir_frame_describes_nonvolatile_register_saves() { + let input = r#" # @fn name=demo symbol=_fn_demo +.globl _fn_demo +_fn_demo: + push rbp + mov rbp, rsp + sub rsp, 96 + mov r10, QWORD PTR [rip + _concat_off] + mov QWORD PTR [rbp - 88], r10 + # save callee-saved registers used by the register allocator + mov QWORD PTR [rbp - 80], rbx + ret + # @endfn name=demo +"#; + let output = transform_for_windows(input); + assert!(output.contains(".seh_savereg rbx, 16")); + assert!(output.contains(".seh_savereg rbx, 16\n .seh_endprologue")); + } + + /// Verifies a frame with a scalar XMM save is left unannotated because PE + /// unwind records restore full 128-bit XMM state and would not match it. + #[test] + fn test_generated_eir_frame_rejects_partial_xmm_save() { + let input = r#" # @fn name=demo symbol=_fn_demo +.globl _fn_demo +_fn_demo: + push rbp + mov rbp, rsp + sub rsp, 96 + # save callee-saved registers used by the register allocator + movsd QWORD PTR [rbp - 72], xmm6 + ret + # @endfn name=demo +"#; + let output = transform_for_windows(input); + assert!(!output.contains(".seh_")); + } + + /// Verifies a merely similar stack sequence without the standard rbp frame + /// shape is not assigned misleading generated-frame unwind metadata. + #[test] + fn test_generated_eir_frame_rejects_nonstandard_prologue() { + let input = r#" # @fn name=demo symbol=_fn_demo +.globl _fn_demo +_fn_demo: + push rbp + sub rsp, 64 + ret + # @endfn name=demo +"#; + let output = transform_for_windows(input); + assert!(!output.contains(".seh_")); + } + + /// Verifies a canonical hand-written runtime frame receives exact unwind metadata. + #[test] + fn test_standard_runtime_frame_receives_unwind_support() { + let input = ".globl __rt_demo\n__rt_demo:\n push rbp\n mov rbp, rsp\n sub rsp, 32\n xor eax, eax\n leave\n ret\n"; + let output = transform_for_windows(input); + assert!(output.contains(".seh_proc __rt_demo")); + assert!(output.contains(".seh_stackalloc 32")); + assert!(output.contains(".seh_endproc")); + } + + /// Verifies a frameless fast path followed by an independent slow frame gets its own PE range. + #[test] + fn test_independent_internal_frame_is_promoted_and_described() { + let input = ".globl __rt_demo\n__rt_demo:\n test eax, eax\n jz __rt_demo_slow\n ret\n__rt_demo_slow:\n push rbp\n mov rbp, rsp\n sub rsp, 32\n leave\n ret\n"; + let output = transform_for_windows(input); + assert!(output.contains(".L__elephc_unwind_region_0:")); + assert!(output.contains(".seh_proc .L__elephc_unwind_region_0")); + assert!(!output.contains(".globl __elephc_unwind_region_0")); + } + + /// Verifies independently transformed runtime and program objects link without + /// exporting colliding synthetic unwind-region symbols. + #[test] + fn test_synthetic_unwind_regions_do_not_collide_between_coff_objects() { + for tool in ["x86_64-w64-mingw32-as", "x86_64-w64-mingw32-gcc"] { + if std::process::Command::new(tool).arg("--version").output().is_err() { + eprintln!("skipping synthetic unwind link test: {tool} not found"); + return; + } + } + + let runtime = transform_for_windows( + ".intel_syntax noprefix\n.text\n.globl __rt_runtime\n__rt_runtime:\n test eax, eax\n jz __rt_runtime_slow\n ret\n__rt_runtime_slow:\n push rbp\n mov rbp, rsp\n sub rsp, 32\n leave\n ret\n", + ); + let program = transform_for_windows( + ".intel_syntax noprefix\n.text\n.globl _fn_program\n_fn_program:\n test eax, eax\n jz _fn_program_slow\n ret\n_fn_program_slow:\n push rbp\n mov rbp, rsp\n sub rsp, 32\n leave\n ret\n", + ); + let directory = std::env::temp_dir().join(format!( + "elephc-local-unwind-link-{}", + std::process::id() + )); + std::fs::create_dir_all(&directory).expect("create local unwind link directory"); + let runtime_assembly = directory.join("runtime.s"); + let runtime_object = directory.join("runtime.o"); + let program_assembly = directory.join("program.s"); + let program_object = directory.join("program.o"); + let executable = directory.join("combined.exe"); + std::fs::write(&runtime_assembly, runtime).expect("write transformed runtime assembly"); + std::fs::write(&program_assembly, program).expect("write transformed program assembly"); + + for (assembly, object) in [ + (&runtime_assembly, &runtime_object), + (&program_assembly, &program_object), + ] { + let assembled = std::process::Command::new("x86_64-w64-mingw32-as") + .arg("-o") + .arg(object) + .arg(assembly) + .output() + .expect("run MinGW assembler for local unwind link test"); + assert!( + assembled.status.success(), + "MinGW rejected transformed assembly:\n{}", + String::from_utf8_lossy(&assembled.stderr) + ); + } + + let linked = std::process::Command::new("x86_64-w64-mingw32-gcc") + .arg("-nostdlib") + .arg("-Wl,-e,_fn_program") + .arg(&runtime_object) + .arg(&program_object) + .arg("-o") + .arg(&executable) + .output() + .expect("link runtime and program COFF objects"); + let _ = std::fs::remove_dir_all(&directory); + assert!( + linked.status.success(), + "synthetic unwind symbols collided across COFF objects:\n{}", + String::from_utf8_lossy(&linked.stderr) + ); + } + + /// Verifies an ambiguous runtime stack adjustment is never described as a fixed frame. + #[test] + fn test_runtime_frame_rejects_dynamic_stack_adjustment() { + let input = ".globl __rt_demo\n__rt_demo:\n push rbp\n mov rbp, rsp\n sub rsp, 256\n sub rsp, 16\n call __rt_other\n add rsp, 16\n leave\n ret\n"; + let output = transform_for_windows(input); + assert!(!output.contains(".seh_")); + } + + /// Verifies an unrepresented nonvolatile-register push refuses runtime metadata. + #[test] + fn test_runtime_frame_rejects_unrepresented_nonvolatile_push() { + let input = ".globl __rt_demo\n__rt_demo:\n push rbp\n mov rbp, rsp\n sub rsp, 32\n push rsi\n pop rsi\n leave\n ret\n"; + let output = transform_for_windows(input); + assert!(!output.contains(".seh_")); + } + + /// Verifies an encodable frame pointer makes transient shadow-space changes unwindable. + #[test] + fn test_runtime_frame_describes_dynamic_rsp_with_frame_pointer() { + let input = ".globl __rt_demo\n__rt_demo:\n push rbp\n mov rbp, rsp\n sub rsp, 32\n sub rsp, 32\n call __rt_other\n add rsp, 32\n add rsp, 32\n pop rbp\n ret\n"; + let output = transform_for_windows(input); + assert!(output.contains(".seh_setframe rbp, 32")); + assert!(output.contains(".seh_proc __rt_demo")); + } + + /// Verifies an internal callable label can return while the outer RBP frame stays active. + #[test] + fn test_runtime_frame_describes_internal_subroutine_with_frame_pointer() { + let input = ".globl __rt_demo\n__rt_demo:\n push rbp\n mov rbp, rsp\n sub rsp, 32\n call __rt_demo_inner\n add rsp, 32\n pop rbp\n ret\n__rt_demo_inner:\n xor eax, eax\n ret\n"; + let output = transform_for_windows(input); + assert!(output.contains(".seh_setframe rbp, 32")); + assert!(output.contains(".seh_proc __rt_demo")); + } + + /// Verifies a live-frame tail call is rejected while a torn-down tail call stays harmless. + #[test] + fn test_runtime_frame_rejects_live_frame_tail_call() { + let input = ".globl __rt_demo\n__rt_demo:\n push rbp\n mov rbp, rsp\n sub rsp, 32\n jmp __rt_other\n"; + let output = transform_for_windows(input); + assert!(!output.contains(".seh_")); + } + + /// Verifies the audited mixed add entry can share the identical mul/common frame record. + #[test] + fn test_mixed_numeric_equivalent_frame_tail_is_described() { + let input = ".globl __rt_mixed_numeric_add\n__rt_mixed_numeric_add:\n push rbp\n mov rbp, rsp\n sub rsp, 80\n mov r10, 0\n jmp __rt_mixed_numeric_common_linux_x86_64\n.globl __rt_mixed_numeric_mul\n__rt_mixed_numeric_mul:\n push rbp\n mov rbp, rsp\n sub rsp, 80\n__rt_mixed_numeric_common_linux_x86_64:\n add rsp, 80\n pop rbp\n ret\n"; + let output = transform_for_windows(input); + assert!(output.contains(".seh_proc __rt_mixed_numeric_add")); + assert!(output.contains(".seh_proc __rt_mixed_numeric_mul")); + } + + /// Measures the all-features runtime audit and keeps both accepted and refused + /// hand-written frame populations non-empty. + #[test] + fn test_full_windows_runtime_has_conservative_unwind_coverage() { + let target = crate::codegen::platform::Target::new( + crate::codegen::platform::Platform::Windows, + crate::codegen::platform::Arch::X86_64, + ); + let raw = crate::codegen::generate_runtime_with_features_pic( + 8 * 1024 * 1024, + target, + crate::codegen::RuntimeFeatures::all(), + false, + ); + let transformed = transform_for_windows(&raw); + let canonical_frames = raw + .match_indices(" push rbp\n mov rbp, rsp\n sub rsp, ") + .count(); + let described_runtime_frames = transformed.matches(".seh_proc ").count(); + let raw_lines = raw.lines().collect::>(); + let mut runtime_functions = 0usize; + let mut leaf_functions = 0usize; + let mut rejection_counts = std::collections::HashMap::new(); + let mut rejected_symbols = Vec::new(); + for (index, line) in raw_lines.iter().enumerate() { + let Some(symbol) = line.trim().strip_prefix(".globl ") else { + continue; + }; + if !symbol.starts_with("__rt_") { + continue; + } + runtime_functions += 1; + let end = function_segment_end(&raw_lines, index); + let segment = &raw_lines[index..end]; + if is_leaf_without_unwind_state(segment) { + leaf_functions += 1; + } + if let Some(metadata) = standard_frame_metadata(segment, symbol) { + if let Err(reason) = audit_runtime_frame(segment, &metadata) { + *rejection_counts.entry(reason).or_insert(0usize) += 1; + rejected_symbols.push((symbol, reason)); + } + } + } + let functions_requiring_unwind = runtime_functions - leaf_functions; + let rejected_canonical_frames = rejection_counts.values().sum::(); + eprintln!( + "Windows runtime unwind audit: {runtime_functions} functions, {leaf_functions} true leaves, {functions_requiring_unwind} requiring unwind state; {described_runtime_frames}/{canonical_frames} canonical frame regions described, {rejected_canonical_frames} global frames rejected {rejection_counts:?}: {rejected_symbols:?}" + ); + assert!(canonical_frames > 0, "runtime audit fixture has no canonical frames"); + assert!( + rejection_counts.is_empty(), + "runtime globals with rejected canonical frames remain: {rejected_symbols:?}" + ); + assert_eq!( + described_runtime_frames, canonical_frames, + "every canonical stack-mutating region must have exactly one PE unwind record" + ); + assert!(leaf_functions > 0, "runtime audit found no true leaf helpers"); + assert!( + described_runtime_frames <= functions_requiring_unwind, + "described more frames than the non-leaf population" + ); + } + + /// Assembles the all-features runtime with MinGW and verifies decoded PE unwind tables. + #[test] + fn test_full_windows_runtime_unwind_metadata_assembles_and_decodes() { + if std::process::Command::new("x86_64-w64-mingw32-as") + .arg("--version") + .output() + .is_err() + || std::process::Command::new("x86_64-w64-mingw32-objdump") + .arg("--version") + .output() + .is_err() + { + eprintln!("skipping runtime unwind object test: MinGW tools not found"); + return; + } + + let target = crate::codegen::platform::Target::new( + crate::codegen::platform::Platform::Windows, + crate::codegen::platform::Arch::X86_64, + ); + let transformed = transform_for_windows( + &crate::codegen::generate_runtime_with_features_pic( + 8 * 1024 * 1024, + target, + crate::codegen::RuntimeFeatures::all(), + false, + ), + ); + let directory = std::env::temp_dir().join(format!( + "elephc-runtime-unwind-{}", + std::process::id() + )); + std::fs::create_dir_all(&directory).expect("create runtime unwind test directory"); + let assembly_path = directory.join("runtime.s"); + let object_path = directory.join("runtime.o"); + std::fs::write(&assembly_path, transformed).expect("write transformed runtime assembly"); + + let assembled = std::process::Command::new("x86_64-w64-mingw32-as") + .arg("-o") + .arg(&object_path) + .arg(&assembly_path) + .output() + .expect("run MinGW assembler"); + assert!( + assembled.status.success(), + "MinGW rejected runtime unwind metadata:\n{}", + String::from_utf8_lossy(&assembled.stderr) + ); + let inspected = std::process::Command::new("x86_64-w64-mingw32-objdump") + .arg("-x") + .arg(&object_path) + .output() + .expect("run MinGW objdump"); + let headers = String::from_utf8_lossy(&inspected.stdout); + let _ = std::fs::remove_dir_all(&directory); + assert!(inspected.status.success(), "objdump failed to inspect runtime object"); + assert!(headers.contains(".pdata"), "runtime object has no .pdata section"); + assert!(headers.contains(".xdata"), "runtime object has no .xdata section"); + assert!( + headers.contains("The Function Table") && headers.contains("UnwindData"), + "objdump did not decode the runtime function table" + ); + } + + /// Helper for tests: returns true if any line is a standalone `syscall`. + fn contains_standalone_syscall(asm: &str) -> bool { + asm.lines().any(is_syscall_line) + } +} diff --git a/src/codegen_support/prescan.rs b/src/codegen_support/prescan.rs index 327a40ddea..1dc87522c7 100644 --- a/src/codegen_support/prescan.rs +++ b/src/codegen_support/prescan.rs @@ -25,10 +25,15 @@ use crate::types::PhpType; /// Seeds the constant map with built-in PHP constants and user-defined constants. /// /// Built-in constants include platform-specific values (e.g., `FNM_*` flags differ -/// between macOS and Linux), `PATHINFO_*` bitmask values, `ENT_*` HTML-escaping flags, -/// stream handles (`STDIN`/`STDOUT`/`STDERR`), `LOCK_*` values, array callback-mode -/// constants, `JSON_*` integer constants, and `PREG_*` integer constants. User constants -/// come from `const` declarations and `define()` calls discovered by `collect_constant_decls`. +/// between macOS and Linux), the target-aware path constants `PHP_OS`, `PHP_OS_FAMILY`, +/// `PHP_EOL`, `DIRECTORY_SEPARATOR`, and `PATH_SEPARATOR` (which differ on Windows), +/// the emulated-PHP version constants (`PHP_VERSION`, `PHP_MAJOR_VERSION`, +/// `PHP_MINOR_VERSION`, `PHP_RELEASE_VERSION`, `PHP_VERSION_ID`, `PHP_EXTRA_VERSION`), +/// `PHP_INT_SIZE`, `PATHINFO_*` bitmask values, `ENT_*` HTML-escaping flags, stream +/// handles (`STDIN`/`STDOUT`/`STDERR`), +/// `LOCK_*` values, array callback-mode constants, `JSON_*` integer constants, and +/// `PREG_*` integer constants. User constants come from `const` declarations and +/// `define()` calls discovered by `collect_constant_decls`. pub(crate) fn collect_constants( program: &Program, target_platform: Platform, @@ -41,6 +46,67 @@ pub(crate) fn collect_constants( PhpType::Str, ), ); + constants.insert( + "PHP_EOL".to_string(), + ( + ExprKind::StringLiteral(target_platform.php_eol().to_string()), + PhpType::Str, + ), + ); + constants.insert( + "DIRECTORY_SEPARATOR".to_string(), + ( + ExprKind::StringLiteral(target_platform.directory_separator().to_string()), + PhpType::Str, + ), + ); + constants.insert( + "PATH_SEPARATOR".to_string(), + ( + ExprKind::StringLiteral(target_platform.path_separator().to_string()), + PhpType::Str, + ), + ); + constants.insert( + "PHP_OS_FAMILY".to_string(), + ( + ExprKind::StringLiteral(target_platform.php_os_family().to_string()), + PhpType::Str, + ), + ); + constants.insert( + "PHP_INT_SIZE".to_string(), + (ExprKind::IntLiteral(8), PhpType::Int), + ); + // elephc's emulated PHP compatibility level (php-src parity target), not the + // compiler's own package version — see `phpversion()` for the latter. + constants.insert( + "PHP_VERSION".to_string(), + ( + ExprKind::StringLiteral("8.4.0".to_string()), + PhpType::Str, + ), + ); + constants.insert( + "PHP_MAJOR_VERSION".to_string(), + (ExprKind::IntLiteral(8), PhpType::Int), + ); + constants.insert( + "PHP_MINOR_VERSION".to_string(), + (ExprKind::IntLiteral(4), PhpType::Int), + ); + constants.insert( + "PHP_RELEASE_VERSION".to_string(), + (ExprKind::IntLiteral(0), PhpType::Int), + ); + constants.insert( + "PHP_VERSION_ID".to_string(), + (ExprKind::IntLiteral(80400), PhpType::Int), + ); + constants.insert( + "PHP_EXTRA_VERSION".to_string(), + (ExprKind::StringLiteral(String::new()), PhpType::Str), + ); constants.insert( "SID".to_string(), (ExprKind::StringLiteral(String::new()), PhpType::Str), @@ -74,7 +140,7 @@ pub(crate) fn collect_constants( let (fnm_noescape, fnm_pathname) = match target_platform { Platform::MacOS => (1, 2), Platform::Linux => (2, 1), - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => (2, 1), }; constants.insert( "FNM_NOESCAPE".to_string(), @@ -234,17 +300,6 @@ pub(crate) fn collect_constants( PhpType::Float, ), ); - constants.insert( - "PHP_EOL".to_string(), - (ExprKind::StringLiteral("\n".to_string()), PhpType::Str), - ); - constants.insert( - "DIRECTORY_SEPARATOR".to_string(), - ( - ExprKind::StringLiteral(std::path::MAIN_SEPARATOR.to_string()), - PhpType::Str, - ), - ); collect_constant_decls(program, &mut constants); constants } @@ -301,7 +356,6 @@ fn constant_expr_type(kind: &ExprKind) -> PhpType { } } - #[cfg(test)] mod tests { use super::*; @@ -314,6 +368,17 @@ mod tests { } } + /// Extracts a string constant from a collected test registry. + fn string_constant<'a>( + constants: &'a HashMap, + name: &str, + ) -> &'a str { + match &constants[name].0 { + ExprKind::StringLiteral(value) => value, + _ => panic!("{name} is not a string constant"), + } + } + /// Verifies fnmatch constants follow target platform. #[test] fn test_fnmatch_constants_follow_target_platform() { @@ -329,4 +394,14 @@ mod tests { assert_eq!(int_constant(&linux, "FNM_PERIOD"), 4); assert_eq!(int_constant(&linux, "FNM_CASEFOLD"), 16); } + + /// Verifies target-specific path and line-ending constants are not + /// overwritten with values from the compiler host. + #[test] + fn test_windows_text_and_path_constants_follow_target_platform() { + let windows = collect_constants(&vec![], Platform::Windows); + assert_eq!(string_constant(&windows, "PHP_EOL"), "\r\n"); + assert_eq!(string_constant(&windows, "DIRECTORY_SEPARATOR"), "\\"); + assert_eq!(string_constant(&windows, "PATH_SEPARATOR"), ";"); + } } diff --git a/src/codegen_support/runtime/arrays/array_filter.rs b/src/codegen_support/runtime/arrays/array_filter.rs index 1d38734a96..06e7866c74 100644 --- a/src/codegen_support/runtime/arrays/array_filter.rs +++ b/src/codegen_support/runtime/arrays/array_filter.rs @@ -205,7 +205,7 @@ fn emit_array_filter_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("je __rt_array_filter_call"); // call key-only callback without environment emitter.instruction("mov rsi, QWORD PTR [rbp - 64]"); // pass capture environment after key argument emitter.label("__rt_array_filter_call"); - emitter.instruction("call r12"); // invoke the user callback with the current source element and read the truthy result from rax + emitter.emit_platform_callback_call("r12", 3); emitter.instruction("test rax, rax"); // check whether the callback reported a truthy keep/skip decision emitter.instruction("jz __rt_array_filter_skip"); // skip copying the element when the callback returned zero / false emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the destination array pointer after the callback clobbered caller-saved registers @@ -234,3 +234,39 @@ fn emit_array_filter_linux_x86_64(emitter: &mut Emitter) { ARRAY_FILTER_MODE_MSG_LEN, ); } + +#[cfg(test)] +mod tests { + use crate::codegen_support::emit::Emitter; + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::*; + + /// Verifies the windows-x86_64 `__rt_array_filter` callback call site emits + /// the reverse-ABI SysV->MSx64 remap immediately before the indirect + /// `call r12` into the generated callback (finding F1, reverse-ABI): without + /// it, the generated callback would read its value/key/env arguments from + /// the wrong registers on windows-x86_64. + #[test] + fn test_windows_x86_64_array_filter_remaps_before_indirect_callback_call() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_array_filter(&mut emitter); + let asm = emitter.output(); + + let remap_idx = asm.find("mov rcx, rdi").expect("expected SysV->MSx64 remap"); + let call_idx = asm.find("call r11").expect("expected relocated indirect call r11"); + assert!(remap_idx < call_idx, "remap must precede the indirect callback call"); + } + + /// Verifies linux-x86_64 emission stays byte-identical to before the + /// reverse-ABI remap was introduced: the remap is windows-x86_64-only, so a + /// linux-x86_64 build must never see a `mov rcx, rdi` instruction. + #[test] + fn test_linux_x86_64_array_filter_has_no_reverse_abi_remap() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_array_filter(&mut emitter); + let asm = emitter.output(); + + assert!(!asm.contains("mov rcx, rdi")); + } +} diff --git a/src/codegen_support/runtime/arrays/array_filter_refcounted.rs b/src/codegen_support/runtime/arrays/array_filter_refcounted.rs index 0c2a039600..b56e70d34c 100644 --- a/src/codegen_support/runtime/arrays/array_filter_refcounted.rs +++ b/src/codegen_support/runtime/arrays/array_filter_refcounted.rs @@ -266,7 +266,7 @@ fn emit_array_filter_refcounted_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("je __rt_array_filter_ref_call"); // call key-only callback without environment emitter.instruction("mov rsi, QWORD PTR [rbp - 64]"); // pass capture environment after key argument emitter.label("__rt_array_filter_ref_call"); - emitter.instruction("call r12"); // invoke the user callback with the current borrowed payload and read the truthy result from rax + emitter.emit_platform_callback_call("r12", 4); emitter.instruction("test rax, rax"); // check whether the callback reported a truthy keep/skip decision emitter.instruction("jz __rt_array_filter_ref_skip"); // skip retaining/copying the payload when the callback returned zero / false emitter.instruction("cmp QWORD PTR [rbp - 72], 16"); // is the kept element a string slot? diff --git a/src/codegen_support/runtime/arrays/array_find_any_all.rs b/src/codegen_support/runtime/arrays/array_find_any_all.rs index cd02559f88..4eef0315ce 100644 --- a/src/codegen_support/runtime/arrays/array_find_any_all.rs +++ b/src/codegen_support/runtime/arrays/array_find_any_all.rs @@ -141,7 +141,7 @@ fn emit_array_find_any_all_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jz __rt_array_find_any_all_call"); // no environment keeps the one-argument callback ABI emitter.instruction("mov rsi, r14"); // pass the environment as the second callback argument emitter.label("__rt_array_find_any_all_call"); - emitter.instruction("call r12"); // call the predicate callback; truthy result in rax + emitter.emit_platform_callback_call("r12", 2); emitter.instruction("mov r11, QWORD PTR [rbp - 40]"); // reload the mode selector emitter.instruction("test r11, r11"); // mode 0 is find? emitter.instruction("jz __rt_array_find_any_all_find"); // handle the find mode diff --git a/src/codegen_support/runtime/arrays/array_get_mixed_key.rs b/src/codegen_support/runtime/arrays/array_get_mixed_key.rs index 4dd0e0f20c..1b6c0ea2d7 100644 --- a/src/codegen_support/runtime/arrays/array_get_mixed_key.rs +++ b/src/codegen_support/runtime/arrays/array_get_mixed_key.rs @@ -309,20 +309,20 @@ fn emit_array_get_mixed_key_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_array_get_mixed_key_hash"); emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // rsi = key_lo emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // rdx = key_hi - emitter.instruction("call __rt_hash_get"); // rax=found, rsi=value_lo, rdx=value_hi, rcx=value_tag + emitter.instruction("call __rt_hash_get"); // rax=found, rdi=value_lo, rsi=value_hi, rcx=value_tag emitter.instruction("test rax, rax"); // miss → null emitter.instruction("je __rt_array_get_mixed_key_hash_missing"); // miss → optional warning + null emitter.instruction("cmp rcx, 7"); // is the hash entry already a boxed Mixed? emitter.instruction("jne __rt_array_get_mixed_key_hash_box"); // no → box (lo, hi, tag) into a fresh Mixed cell - emitter.instruction("mov rax, rsi"); // yes → move the stored Mixed cell into the return register + emitter.instruction("mov rax, rdi"); // yes → move the stored Mixed cell (value_lo) into the return register emitter.instruction("call __rt_incref"); // retain the stored Mixed cell so the caller owns the returned result emitter.instruction("mov rsp, rbp"); // release the helper frame emitter.instruction("pop rbp"); // restore caller frame pointer emitter.instruction("ret"); // return Mixed* in rax emitter.label("__rt_array_get_mixed_key_hash_box"); emitter.instruction("mov rax, rcx"); // rax = value_tag (mixed_from_value first arg) - emitter.instruction("mov rdi, rsi"); // rdi = value_lo from hash_get - emitter.instruction("mov rsi, rdx"); // rsi = value_hi from hash_get + emitter.instruction("mov rdi, rdi"); // rdi = value_lo (already in place from __rt_hash_get) + emitter.instruction("mov rsi, rsi"); // rsi = value_hi (already in place from __rt_hash_get) emitter.instruction("call __rt_mixed_from_value"); // box the hash entry into a Mixed cell emitter.instruction("mov rsp, rbp"); // release the helper frame emitter.instruction("pop rbp"); // restore caller frame pointer diff --git a/src/codegen_support/runtime/arrays/array_map.rs b/src/codegen_support/runtime/arrays/array_map.rs index 3baeeb74ab..7cffe25938 100644 --- a/src/codegen_support/runtime/arrays/array_map.rs +++ b/src/codegen_support/runtime/arrays/array_map.rs @@ -177,7 +177,7 @@ fn emit_array_map_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("je __rt_array_map_call"); // keep legacy string callback ABI when no environment is present emitter.instruction("mov rdx, QWORD PTR [rbp - 48]"); // pass capture environment after the string ptr/len pair emitter.label("__rt_array_map_call"); - emitter.instruction("call r12"); // invoke the user callback with the current element and read the transformed value from rax + emitter.emit_platform_callback_call("r12", 3); emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the destination array pointer after the callback clobbered caller-saved registers emitter.instruction("mov QWORD PTR [r10 + r13 * 8 + 24], rax"); // store the transformed value into the matching destination-array element slot emitter.instruction("add r13, 1"); // advance the loop index after storing the transformed destination element @@ -193,3 +193,39 @@ fn emit_array_map_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the caller frame pointer before returning the mapped array pointer emitter.instruction("ret"); // return the mapped destination array pointer in rax } + +#[cfg(test)] +mod tests { + use crate::codegen_support::emit::Emitter; + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::*; + + /// Verifies the windows-x86_64 `__rt_array_map` callback call site emits the + /// reverse-ABI SysV->MSx64 remap immediately before the indirect `call r12` + /// into the generated callback (finding F1, reverse-ABI): without it, the + /// generated callback would read its element/env arguments from the wrong + /// registers on windows-x86_64. + #[test] + fn test_windows_x86_64_array_map_remaps_before_indirect_callback_call() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_array_map(&mut emitter); + let asm = emitter.output(); + + let remap_idx = asm.find("mov rcx, rdi").expect("expected SysV->MSx64 remap"); + let call_idx = asm.find("call r11").expect("expected relocated indirect call r11"); + assert!(remap_idx < call_idx, "remap must precede the indirect callback call"); + } + + /// Verifies linux-x86_64 emission stays byte-identical to before the + /// reverse-ABI remap was introduced: the remap is windows-x86_64-only, so a + /// linux-x86_64 build must never see a `mov rcx, rdi` instruction. + #[test] + fn test_linux_x86_64_array_map_has_no_reverse_abi_remap() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_array_map(&mut emitter); + let asm = emitter.output(); + + assert!(!asm.contains("mov rcx, rdi")); + } +} diff --git a/src/codegen_support/runtime/arrays/array_map_mixed.rs b/src/codegen_support/runtime/arrays/array_map_mixed.rs index 362bdd2d53..6cb48d6de7 100644 --- a/src/codegen_support/runtime/arrays/array_map_mixed.rs +++ b/src/codegen_support/runtime/arrays/array_map_mixed.rs @@ -142,7 +142,7 @@ fn emit_array_map_mixed_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rdx, QWORD PTR [rbp - 48]"); // pass descriptor environment after the string pair emitter.label("__rt_array_map_mixed_call"); - emitter.instruction("call r12"); // invoke descriptor callback and receive owned boxed Mixed in rax + emitter.emit_platform_callback_call("r12", 3); emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload destination array pointer after callback clobbers caller-saved regs emitter.instruction("mov QWORD PTR [r10 + r13 * 8 + 24], rax"); // transfer owned boxed Mixed pointer into destination slot emitter.instruction("add r13, 1"); // advance loop index after storing the mapped Mixed value diff --git a/src/codegen_support/runtime/arrays/array_map_str.rs b/src/codegen_support/runtime/arrays/array_map_str.rs index 830724c744..569e8d3390 100644 --- a/src/codegen_support/runtime/arrays/array_map_str.rs +++ b/src/codegen_support/runtime/arrays/array_map_str.rs @@ -281,7 +281,7 @@ fn emit_array_map_str_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rdx, QWORD PTR [rbp - 56]"); // pass capture environment after the string pointer/length pair emitter.label("__rt_array_map_str_call"); - emitter.instruction("call r12"); // invoke the user callback and read the produced string result from rax=ptr, rdx=len + emitter.emit_platform_callback_call("r12", 3); emitter.instruction("mov rsi, rax"); // move the callback-produced string pointer into the x86_64 array-push string payload register emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the destination array pointer into the x86_64 array-push receiver register emitter.instruction("call __rt_array_push_str"); // persist and append the callback-produced string into the destination array, returning the possibly-grown array pointer @@ -346,7 +346,7 @@ fn emit_array_map_str_owned_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rdx, QWORD PTR [rbp - 48]"); // pass descriptor environment after the string pair emitter.label("__rt_array_map_str_owned_call"); - emitter.instruction("call r12"); // invoke descriptor callback and receive owned string in rax/rdx + emitter.emit_platform_callback_call("r12", 3); emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload destination array pointer after callback clobbers caller-saved regs emitter.instruction("mov rcx, r13"); // copy loop index before destination string-slot scaling emitter.instruction("shl rcx, 4"); // compute 16-byte destination string slot offset diff --git a/src/codegen_support/runtime/arrays/array_rand.rs b/src/codegen_support/runtime/arrays/array_rand.rs index 44cba28d20..a998d7d2c3 100644 --- a/src/codegen_support/runtime/arrays/array_rand.rs +++ b/src/codegen_support/runtime/arrays/array_rand.rs @@ -55,6 +55,28 @@ fn emit_array_rand_linux_x86_64(emitter: &mut Emitter) { emitter.label_global("__rt_array_rand"); emitter.instruction("mov rdi, QWORD PTR [rdi]"); // load the source indexed-array logical length into the x86_64 random-uniform bound register + emitter.instruction("sub rsp, 8"); // align the x86_64 stack before the nested random-uniform helper call emitter.instruction("call __rt_random_uniform"); // sample a random scalar index in the half-open range [0, length) + emitter.instruction("add rsp, 8"); // restore the caller stack after the nested random-uniform helper call emitter.instruction("ret"); // return the sampled scalar index in rax } + +#[cfg(test)] +mod tests { + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::*; + + /// Verifies the x86_64 array-rand wrapper aligns the stack before its + /// nested helper call, including when the final PE runs under MSx64. + #[test] + fn x86_64_array_rand_aligns_nested_call() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_array_rand(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains("sub rsp, 8\n")); + assert!(asm.contains("call __rt_random_uniform\n")); + assert!(asm.contains("add rsp, 8\n")); + } +} diff --git a/src/codegen_support/runtime/arrays/array_reduce.rs b/src/codegen_support/runtime/arrays/array_reduce.rs index a453d4fdd6..d239b48508 100644 --- a/src/codegen_support/runtime/arrays/array_reduce.rs +++ b/src/codegen_support/runtime/arrays/array_reduce.rs @@ -122,7 +122,7 @@ fn emit_array_reduce_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jz __rt_array_reduce_call_linux_x86_64"); // keep legacy two-argument callback ABI when no environment is present emitter.instruction("mov rdx, r15"); // pass capture environment after accumulator and element emitter.label("__rt_array_reduce_call_linux_x86_64"); - emitter.instruction("call r12"); // invoke the user callback with (accumulator, element) and read the new accumulator from rax + emitter.emit_platform_callback_call("r12", 3); emitter.instruction("mov r14, rax"); // update the live accumulator with the callback result before the next iteration emitter.instruction("add r13, 1"); // advance the source index after folding the current element emitter.instruction("jmp __rt_array_reduce_loop"); // continue reducing until the whole source array has been consumed diff --git a/src/codegen_support/runtime/arrays/array_slice.rs b/src/codegen_support/runtime/arrays/array_slice.rs index e714de7705..64909c3ee2 100644 --- a/src/codegen_support/runtime/arrays/array_slice.rs +++ b/src/codegen_support/runtime/arrays/array_slice.rs @@ -54,8 +54,14 @@ pub fn emit_array_slice(emitter: &mut Emitter) { emitter.instruction("cmp x1, x9"); // check if offset >= array length emitter.instruction("b.ge __rt_array_slice_empty"); // if so, result is empty array emitter.instruction("sub x3, x9, x1"); // x3 = max possible length = array_len - offset - emitter.instruction("cmn x2, #1"); // check if length == -1 (to end) - emitter.instruction("csel x2, x3, x2, eq"); // if length == -1, use remaining length + // php reads a negative length as "stop that many elements before the end of the + // window", not as "select nothing". + emitter.instruction("cmp x2, #0"); // is the requested length negative? + emitter.instruction("b.ge __rt_array_slice_len_clamp"); // a non-negative length only needs the upper clamp + emitter.instruction("add x2, x3, x2"); // stop that many elements before the end + emitter.instruction("cmp x2, #0"); // did the omission consume the whole window? + emitter.instruction("csel x2, xzr, x2, lt"); // an over-long omission selects nothing + emitter.label("__rt_array_slice_len_clamp"); emitter.instruction("cmp x2, x3"); // clamp length to max possible emitter.instruction("csel x2, x3, x2, gt"); // if length > remaining, use remaining emitter.instruction("str x1, [sp, #16]"); // save computed offset @@ -126,9 +132,14 @@ fn emit_array_slice_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jge __rt_array_slice_empty_x86"); // return an empty indexed array when the requested slice offset starts beyond the source length emitter.instruction("mov rcx, r10"); // seed the maximum removable-length scratch register from the source indexed-array logical length emitter.instruction("sub rcx, rsi"); // compute the remaining scalar payload count from the normalized slice offset to the end of the source indexed array - emitter.instruction("cmp rdx, -1"); // detect the sentinel that means array_slice should run until the end of the source indexed array - emitter.instruction("jne __rt_array_slice_known_len_x86"); // keep the explicit requested slice length when the caller did not use the until-end sentinel - emitter.instruction("mov rdx, rcx"); // replace the until-end sentinel with the remaining scalar payload count in the source indexed array + // php reads a negative length as "stop that many elements before the end of the + // window", not as "select nothing". + emitter.instruction("cmp rdx, 0"); // is the requested length negative? + emitter.instruction("jge __rt_array_slice_known_len_x86"); // a non-negative length only needs the upper clamp + emitter.instruction("add rdx, rcx"); // stop that many elements before the end + emitter.instruction("cmp rdx, 0"); // did the omission consume the whole window? + emitter.instruction("jge __rt_array_slice_known_len_x86"); // the window still holds elements + emitter.instruction("xor edx, edx"); // an over-long omission selects nothing emitter.label("__rt_array_slice_known_len_x86"); emitter.instruction("cmp rdx, rcx"); // clamp the requested slice length so it cannot extend beyond the source indexed-array bounds diff --git a/src/codegen_support/runtime/arrays/array_slice_refcounted.rs b/src/codegen_support/runtime/arrays/array_slice_refcounted.rs index 54930b7d17..37b440e0ad 100644 --- a/src/codegen_support/runtime/arrays/array_slice_refcounted.rs +++ b/src/codegen_support/runtime/arrays/array_slice_refcounted.rs @@ -56,8 +56,14 @@ pub fn emit_array_slice_refcounted(emitter: &mut Emitter) { emitter.instruction("cmp x1, x9"); // compare offset with source length emitter.instruction("b.ge __rt_array_slice_ref_empty"); // return empty array when offset is out of range emitter.instruction("sub x3, x9, x1"); // compute maximum possible slice length - emitter.instruction("cmn x2, #1"); // check whether requested length is -1 - emitter.instruction("csel x2, x3, x2, eq"); // use remaining length when caller requested -1 + // php reads a negative length as "stop that many elements before the end of the + // window", not as "select nothing". + emitter.instruction("cmp x2, #0"); // is the requested length negative? + emitter.instruction("b.ge __rt_array_slice_ref_len_clamp"); // a non-negative length only needs the upper clamp + emitter.instruction("add x2, x3, x2"); // stop that many elements before the end + emitter.instruction("cmp x2, #0"); // did the omission consume the whole window? + emitter.instruction("csel x2, xzr, x2, lt"); // an over-long omission selects nothing + emitter.label("__rt_array_slice_ref_len_clamp"); emitter.instruction("cmp x2, x3"); // compare requested length with remaining length emitter.instruction("csel x2, x3, x2, gt"); // clamp requested length to remaining length emitter.instruction("str x1, [sp, #16]"); // save normalized offset @@ -127,9 +133,14 @@ fn emit_array_slice_refcounted_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jge __rt_array_slice_ref_empty_x86"); // return an empty indexed array when the requested slice offset starts beyond the source length emitter.instruction("mov rcx, r10"); // seed the remaining-window scratch register from the source indexed-array logical length emitter.instruction("sub rcx, rsi"); // compute the remaining refcounted payload count from the normalized slice offset to the end of the source indexed array - emitter.instruction("cmp rdx, -1"); // detect the sentinel that means array_slice should run until the end of the source indexed array - emitter.instruction("jne __rt_array_slice_ref_known_len_x86"); // keep the explicit requested slice length when the caller did not use the until-end sentinel - emitter.instruction("mov rdx, rcx"); // replace the until-end sentinel with the remaining refcounted payload count in the source indexed array + // php reads a negative length as "stop that many elements before the end of the + // window", not as "select nothing". + emitter.instruction("cmp rdx, 0"); // is the requested length negative? + emitter.instruction("jge __rt_array_slice_ref_known_len_x86"); // a non-negative length only needs the upper clamp + emitter.instruction("add rdx, rcx"); // stop that many elements before the end + emitter.instruction("cmp rdx, 0"); // did the omission consume the whole window? + emitter.instruction("jge __rt_array_slice_ref_known_len_x86"); // the window still holds elements + emitter.instruction("xor edx, edx"); // an over-long omission selects nothing emitter.label("__rt_array_slice_ref_known_len_x86"); emitter.instruction("cmp rdx, rcx"); // clamp the requested slice length so it cannot extend beyond the source indexed-array bounds diff --git a/src/codegen_support/runtime/arrays/array_splice.rs b/src/codegen_support/runtime/arrays/array_splice.rs index 6aa48d9ee8..b5eb9a537a 100644 --- a/src/codegen_support/runtime/arrays/array_splice.rs +++ b/src/codegen_support/runtime/arrays/array_splice.rs @@ -53,6 +53,14 @@ pub fn emit_array_splice(emitter: &mut Emitter) { // -- clamp removal length to not exceed array bounds -- emitter.instruction("ldr x3, [x0]"); // x3 = source array length emitter.instruction("sub x4, x3, x1"); // x4 = length - offset (max removable) + // php reads a negative length as "stop that many elements before the end of the + // window", not as "select nothing". + emitter.instruction("cmp x2, #0"); // is the requested length negative? + emitter.instruction("b.ge __rt_array_splice_len_clamp"); // a non-negative length only needs the upper clamp + emitter.instruction("add x2, x4, x2"); // stop that many elements before the end + emitter.instruction("cmp x2, #0"); // did the omission consume the whole window? + emitter.instruction("csel x2, xzr, x2, lt"); // an over-long omission selects nothing + emitter.label("__rt_array_splice_len_clamp"); emitter.instruction("cmp x2, x4"); // compare requested length with max emitter.instruction("csel x2, x4, x2, gt"); // clamp to max if too large emitter.instruction("str x2, [sp, #16]"); // save clamped removal length @@ -140,9 +148,14 @@ fn emit_array_splice_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the source indexed-array logical length before clamping the requested removal length emitter.instruction("mov rcx, r10"); // seed the remaining-window scratch register from the source indexed-array logical length emitter.instruction("sub rcx, rsi"); // compute the maximum removable scalar payload count from the requested splice offset - emitter.instruction("cmp rdx, -1"); // detect the sentinel that means array_splice should remove until the end of the source indexed array - emitter.instruction("jne __rt_array_splice_known_len_x86"); // keep the explicit requested removal length when the caller did not use the until-end sentinel - emitter.instruction("mov rdx, rcx"); // replace the until-end sentinel with the remaining scalar payload count in the source indexed array + // php reads a negative length as "stop that many elements before the end of the + // window", not as "select nothing". + emitter.instruction("cmp rdx, 0"); // is the requested length negative? + emitter.instruction("jge __rt_array_splice_known_len_x86"); // a non-negative length only needs the upper clamp + emitter.instruction("add rdx, rcx"); // stop that many elements before the end + emitter.instruction("cmp rdx, 0"); // did the omission consume the whole window? + emitter.instruction("jge __rt_array_splice_known_len_x86"); // the window still holds elements + emitter.instruction("xor edx, edx"); // an over-long omission selects nothing emitter.label("__rt_array_splice_known_len_x86"); emitter.instruction("cmp rdx, rcx"); // clamp the requested removal length so it never extends beyond the source indexed-array bounds diff --git a/src/codegen_support/runtime/arrays/array_splice_refcounted.rs b/src/codegen_support/runtime/arrays/array_splice_refcounted.rs index 72e8a1706e..b7b0a2c6c3 100644 --- a/src/codegen_support/runtime/arrays/array_splice_refcounted.rs +++ b/src/codegen_support/runtime/arrays/array_splice_refcounted.rs @@ -53,6 +53,14 @@ pub fn emit_array_splice_refcounted(emitter: &mut Emitter) { // -- clamp removal length to not exceed array bounds -- emitter.instruction("ldr x3, [x0]"); // load source array length emitter.instruction("sub x4, x3, x1"); // compute maximum removable length + // php reads a negative length as "stop that many elements before the end of the + // window", not as "select nothing". + emitter.instruction("cmp x2, #0"); // is the requested length negative? + emitter.instruction("b.ge __rt_array_splice_ref_len_clamp"); // a non-negative length only needs the upper clamp + emitter.instruction("add x2, x4, x2"); // stop that many elements before the end + emitter.instruction("cmp x2, #0"); // did the omission consume the whole window? + emitter.instruction("csel x2, xzr, x2, lt"); // an over-long omission selects nothing + emitter.label("__rt_array_splice_ref_len_clamp"); emitter.instruction("cmp x2, x4"); // compare requested length with maximum removable length emitter.instruction("csel x2, x4, x2, gt"); // clamp length to the remaining number of elements emitter.instruction("str x2, [sp, #16]"); // save clamped removal length @@ -131,9 +139,14 @@ fn emit_array_splice_refcounted_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the source indexed-array logical length before clamping the requested removal length emitter.instruction("mov rcx, r10"); // seed the remaining-window scratch register from the source indexed-array logical length emitter.instruction("sub rcx, rsi"); // compute the maximum removable refcounted payload count from the requested splice offset - emitter.instruction("cmp rdx, -1"); // detect the sentinel that means array_splice should remove until the end of the source indexed array - emitter.instruction("jne __rt_array_splice_ref_known_len_x86"); // keep the explicit requested removal length when the caller did not use the until-end sentinel - emitter.instruction("mov rdx, rcx"); // replace the until-end sentinel with the remaining refcounted payload count in the source indexed array + // php reads a negative length as "stop that many elements before the end of the + // window", not as "select nothing". + emitter.instruction("cmp rdx, 0"); // is the requested length negative? + emitter.instruction("jge __rt_array_splice_ref_known_len_x86"); // a non-negative length only needs the upper clamp + emitter.instruction("add rdx, rcx"); // stop that many elements before the end + emitter.instruction("cmp rdx, 0"); // did the omission consume the whole window? + emitter.instruction("jge __rt_array_splice_ref_known_len_x86"); // the window still holds elements + emitter.instruction("xor edx, edx"); // an over-long omission selects nothing emitter.label("__rt_array_splice_ref_known_len_x86"); emitter.instruction("cmp rdx, rcx"); // clamp the requested removal length so it never extends beyond the source indexed-array bounds diff --git a/src/codegen_support/runtime/arrays/array_to_mixed.rs b/src/codegen_support/runtime/arrays/array_to_mixed.rs index f169abb037..dd9a25cc7f 100644 --- a/src/codegen_support/runtime/arrays/array_to_mixed.rs +++ b/src/codegen_support/runtime/arrays/array_to_mixed.rs @@ -216,7 +216,7 @@ fn emit_array_to_mixed_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the converted array pointer - emitter.label("__rt_array_to_mixed_x86_box_owned"); + emitter.label_global("__rt_array_to_mixed_x86_box_owned"); emitter.instruction("cmp rax, 4"); // only container-shaped tags can carry the null sentinel emitter.instruction("jl __rt_array_to_mixed_x86_box_owned_frame"); // preserve scalar payloads verbatim emitter.instruction("cmp rax, 6"); // indexed arrays, hashes, and objects occupy tags 4 through 6 diff --git a/src/codegen_support/runtime/arrays/array_udiff_uintersect.rs b/src/codegen_support/runtime/arrays/array_udiff_uintersect.rs index 4c2014afd3..354f4deaef 100644 --- a/src/codegen_support/runtime/arrays/array_udiff_uintersect.rs +++ b/src/codegen_support/runtime/arrays/array_udiff_uintersect.rs @@ -137,7 +137,7 @@ fn emit_array_udiff_uintersect_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jz __rt_array_udiff_uintersect_cmp"); // no environment keeps the two-argument comparator ABI emitter.instruction("mov rdx, r13"); // pass the environment as the third comparator argument emitter.label("__rt_array_udiff_uintersect_cmp"); - emitter.instruction("call r12"); // call cmp(a, b [, env]); zero result means equal + emitter.emit_platform_callback_call("r12", 3); emitter.instruction("test rax, rax"); // did the comparator report equality? emitter.instruction("jz __rt_array_udiff_uintersect_present"); // a zero comparator result means the element is in arr2 emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // reload inner index j diff --git a/src/codegen_support/runtime/arrays/array_walk.rs b/src/codegen_support/runtime/arrays/array_walk.rs index 80a0d4e0fc..75670d92bb 100644 --- a/src/codegen_support/runtime/arrays/array_walk.rs +++ b/src/codegen_support/runtime/arrays/array_walk.rs @@ -105,7 +105,7 @@ fn emit_array_walk_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jz __rt_array_walk_call_linux_x86_64"); // keep legacy one-argument callback ABI when no environment is present emitter.instruction("mov rsi, r14"); // pass capture environment as the wrapper's second argument emitter.label("__rt_array_walk_call_linux_x86_64"); - emitter.instruction("call r12"); // invoke the user callback with the current source element and ignore the scalar return value + emitter.emit_platform_callback_call("r12", 2); emitter.instruction("add r13, 1"); // advance the source index after visiting the current element emitter.instruction("jmp __rt_array_walk_loop"); // continue walking until the whole source array has been visited diff --git a/src/codegen_support/runtime/arrays/array_walk_recursive.rs b/src/codegen_support/runtime/arrays/array_walk_recursive.rs index 795e2b948a..4a90a28d44 100644 --- a/src/codegen_support/runtime/arrays/array_walk_recursive.rs +++ b/src/codegen_support/runtime/arrays/array_walk_recursive.rs @@ -151,7 +151,7 @@ fn emit_array_walk_recursive_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jz __rt_array_walk_recursive_idx_call"); // no environment keeps the one-argument callback ABI emitter.instruction("mov rsi, r14"); // pass the callback environment as the second argument emitter.label("__rt_array_walk_recursive_idx_call"); - emitter.instruction("call r12"); // invoke callback(leaf [, env]); return value discarded + emitter.emit_platform_callback_call("r12", 2); emitter.instruction("mov rax, QWORD PTR [rbp - 48]"); // reload the index after the callback call emitter.instruction("add rax, 1"); // advance to the next element emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the advanced index @@ -188,7 +188,7 @@ fn emit_array_walk_recursive_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jz __rt_array_walk_recursive_hash_call"); // no environment keeps the one-argument callback ABI emitter.instruction("mov rsi, r14"); // pass the callback environment as the second argument emitter.label("__rt_array_walk_recursive_hash_call"); - emitter.instruction("call r12"); // invoke callback(leaf [, env]); return value discarded + emitter.emit_platform_callback_call("r12", 2); emitter.instruction("jmp __rt_array_walk_recursive_hash_loop"); // continue iterating the hash entries emitter.label("__rt_array_walk_recursive_hash_rec"); emitter.instruction("mov rdi, r12"); // pass the callback address to the recursive call diff --git a/src/codegen_support/runtime/arrays/hash_to_mixed.rs b/src/codegen_support/runtime/arrays/hash_to_mixed.rs index 4082f4aa77..b24a8fcae0 100644 --- a/src/codegen_support/runtime/arrays/hash_to_mixed.rs +++ b/src/codegen_support/runtime/arrays/hash_to_mixed.rs @@ -155,7 +155,7 @@ fn emit_hash_to_mixed_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the converted hash pointer - emitter.label("__rt_hash_to_mixed_x86_box_owned"); + emitter.label_global("__rt_hash_to_mixed_x86_box_owned"); emitter.instruction("cmp rax, 4"); // only container-shaped tags can carry the null sentinel emitter.instruction("jl __rt_hash_to_mixed_x86_box_owned_frame"); // preserve scalar payloads verbatim emitter.instruction("cmp rax, 6"); // indexed arrays, hashes, and objects occupy tags 4 through 6 diff --git a/src/codegen_support/runtime/arrays/heap_alloc.rs b/src/codegen_support/runtime/arrays/heap_alloc.rs index 8b5cfa2f11..71021a3cc1 100644 --- a/src/codegen_support/runtime/arrays/heap_alloc.rs +++ b/src/codegen_support/runtime/arrays/heap_alloc.rs @@ -223,6 +223,10 @@ pub fn emit_heap_alloc(emitter: &mut Emitter) { emitter.instruction("b __rt_heap_alloc_count"); // count alloc/live/peak stats and return // -- fatal error: heap memory exhausted -- + // The shared entry preserves this atom for unconditional fatal tails from + // other runtime helpers under macOS `-dead_strip`; local conditional + // branches above still target the following localized label. + emitter.label_shared("__rt_heap_exhausted_entry"); emitter.label("__rt_heap_exhausted"); emitter.instruction("mov x0, #2"); // fd = stderr crate::codegen_support::abi::emit_symbol_address(emitter, "x1", "_heap_err_msg"); @@ -420,6 +424,10 @@ fn emit_heap_alloc_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jmp __rt_heap_alloc_count"); // reuse the shared allocation-accounting path for bumped blocks // -- fatal error: heap memory exhausted -- + // The shared entry preserves this atom for unconditional fatal tails from + // other runtime helpers under macOS `-dead_strip`; local conditional + // branches above still target the following localized label. + emitter.label_shared("__rt_heap_exhausted_entry"); emitter.label("__rt_heap_exhausted"); emitter.instruction("mov edi, 2"); // fd = stderr for the heap exhaustion fatal error message crate::codegen_support::abi::emit_symbol_address(emitter, "rsi", "_heap_err_msg"); diff --git a/src/codegen_support/runtime/arrays/mixed_cast_float.rs b/src/codegen_support/runtime/arrays/mixed_cast_float.rs index c68a411bc2..2ae04fa065 100644 --- a/src/codegen_support/runtime/arrays/mixed_cast_float.rs +++ b/src/codegen_support/runtime/arrays/mixed_cast_float.rs @@ -107,7 +107,7 @@ fn emit_mixed_cast_float_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rax, rdi"); // move the unboxed string pointer into the x86_64 string-result pointer register abi::emit_call_label(emitter, "__rt_cstr"); // materialize a null-terminated copy of the unboxed elefant string payload emitter.instruction("mov rdi, rax"); // pass the temporary C string through the SysV first integer argument register before atof() - emitter.instruction("call atof"); // parse the current C string payload as double + emitter.emit_call_c("atof"); // parse the current C string payload as double emitter.instruction("jmp __rt_mixed_cast_float_done_linux_x86_64"); // return the parsed floating-point string payload emitter.label("__rt_mixed_cast_float_from_float_linux_x86_64"); diff --git a/src/codegen_support/runtime/arrays/mixed_free_deep.rs b/src/codegen_support/runtime/arrays/mixed_free_deep.rs index 47cadd5004..d075623fb8 100644 --- a/src/codegen_support/runtime/arrays/mixed_free_deep.rs +++ b/src/codegen_support/runtime/arrays/mixed_free_deep.rs @@ -10,7 +10,8 @@ //! - Tag 9 (resource) dispatches to a kind-specific destructor stored in the high payload word: //! kind 0 = generic/unknown (no destructor), kind 1 = native stream fd (close), //! kind 2 = HashContext (elephc_crypto_free), kind 3 = popen pipe (__rt_pclose, -//! closes the FILE* and reaps the child), kind 4 = opendir stream (__rt_closedir). +//! closes the FILE* and reaps the child), kind 4 = opendir stream (__rt_closedir), +//! kind 5 = proc_open resource (__rt_proc_close, reaps the child process). //! - Each fd-backed kind skips handles >= 0x40000000: synthetic wrapper handles and //! the -1 sentinel written into the low payload word by an explicit close (see #4) //! so an already-released descriptor is never closed twice. @@ -106,6 +107,10 @@ pub fn emit_mixed_free_deep(emitter: &mut Emitter) { emitter.instruction("b.eq __rt_mixed_free_deep_resource_dir"); // directory streams release their DIR* via __rt_closedir + emitter.instruction("cmp x9, #5"); // is the resource a proc_open process handle? + + emitter.instruction("b.eq __rt_mixed_free_deep_resource_proc"); // proc handles reap the child via __rt_proc_close + emitter.instruction("b __rt_mixed_free_deep_box"); // unknown resource kind, free the box without destructor @@ -158,6 +163,20 @@ pub fn emit_mixed_free_deep(emitter: &mut Emitter) { emitter.instruction("b __rt_mixed_free_deep_box"); // free the mixed box after releasing the directory + emitter.label("__rt_mixed_free_deep_resource_proc"); + emitter.instruction("ldr x0, [x0, #8]"); // load the process descriptor from the low payload word + + emitter.instruction("mov x9, #0x40000000"); // load the synthetic/sentinel handle threshold into a scratch register + + emitter.instruction("cmp x0, x9"); // skip the -1 sentinel left by an explicit proc_close + + emitter.instruction("b.hs __rt_mixed_free_deep_box"); // skip release for already-closed process handles + + emitter.instruction("bl __rt_proc_close"); // proc_close reaps the child process and frees its resources + + emitter.instruction("b __rt_mixed_free_deep_box"); // free the mixed box after releasing the process handle + + emitter.label("__rt_mixed_free_deep_string"); emitter.instruction("ldr x0, [x0, #8]"); // load the boxed string pointer @@ -267,6 +286,10 @@ fn emit_mixed_free_deep_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("je __rt_mixed_free_deep_resource_dir"); // directory streams release their DIR* via __rt_closedir + emitter.instruction("cmp r9, 5"); // is the resource a proc_open process handle? + + emitter.instruction("je __rt_mixed_free_deep_resource_proc"); // proc handles reap the child via __rt_proc_close + emitter.instruction("jmp __rt_mixed_free_deep_box"); // unknown resource kind, free the box without destructor @@ -314,6 +337,18 @@ fn emit_mixed_free_deep_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jmp __rt_mixed_free_deep_box"); // free the mixed box after releasing the directory + emitter.label("__rt_mixed_free_deep_resource_proc"); + emitter.instruction("mov rdi, QWORD PTR [rax + 8]"); // load the process descriptor from the low payload word + + emitter.instruction("cmp rdi, 0x40000000"); // sentinel(-1)/synthetic handle threshold + + emitter.instruction("jae __rt_mixed_free_deep_box"); // skip release for already-closed process handles + + emitter.instruction("call __rt_proc_close"); // proc_close reaps the child process and frees its resources + + emitter.instruction("jmp __rt_mixed_free_deep_box"); // free the mixed box after releasing the process handle + + emitter.label("__rt_mixed_free_deep_string"); emitter.instruction("mov rax, QWORD PTR [rax + 8]"); // load the boxed string pointer from the mixed payload before releasing it diff --git a/src/codegen_support/runtime/arrays/mod.rs b/src/codegen_support/runtime/arrays/mod.rs index 92d2494492..09b525cc1b 100644 --- a/src/codegen_support/runtime/arrays/mod.rs +++ b/src/codegen_support/runtime/arrays/mod.rs @@ -64,6 +64,7 @@ mod array_set_mixed_key; mod array_set_refcounted; mod array_set_str; mod array_rand; +mod random_bytes; mod random_u32; mod random_uniform; mod array_reduce; @@ -268,10 +269,14 @@ pub use array_set_str::emit_array_set_str; /// Emit string indexed-array set helper. pub use array_rand::emit_array_rand; /// Emit random array element helper. +pub use random_bytes::emit_random_bytes; +/// Emit cryptographically secure random-bytes string helper (random_bytes). pub use random_u32::emit_random_u32; /// Emit 32-bit random unsigned integer helper. pub use random_uniform::emit_random_uniform; /// Emit uniform random integer helper. +pub use random_uniform::emit_random_uniform64; +/// Emit 64-bit uniform random integer helper over an inclusive range width. pub use array_reduce::emit_array_reduce; /// Emit array reduce helper. pub use array_replace::emit_array_replace; diff --git a/src/codegen_support/runtime/arrays/random_bytes.rs b/src/codegen_support/runtime/arrays/random_bytes.rs new file mode 100644 index 0000000000..86f5bdc0b0 --- /dev/null +++ b/src/codegen_support/runtime/arrays/random_bytes.rs @@ -0,0 +1,285 @@ +//! Purpose: +//! Emits the `__rt_random_bytes` runtime helper assembly for PHP's `random_bytes()`. +//! Allocates an owned binary string and fills it with cryptographically secure bytes, +//! keeping each supported target's CSPRNG source and ABI in one focused emitter. +//! +//! Called from: +//! - `crate::codegen::runtime::emitters::emit_runtime()` via `crate::codegen::runtime::arrays`. +//! +//! Key details: +//! - `random_bytes()` is a CSPRNG: this helper never falls back to a weaker source. macOS +//! uses libc `arc4random_buf`; Linux uses the `getrandom` syscall; the x86_64 form also +//! serves Windows through the syscall→shim transform (`__rt_sys_getrandom` = BCryptGenRandom). +//! - A length below 1 or an unavailable entropy source aborts with a fatal diagnostic and +//! `exit(1)` rather than returning weak or empty output. + +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; +use crate::codegen::platform::Platform; +use crate::codegen::runtime::data::{RANDOM_BYTES_LENGTH_MSG, RANDOM_BYTES_SOURCE_MSG}; + +/// Emits the `__rt_random_bytes` runtime helper for `random_bytes(int $length): string`. +/// +/// Dispatches to the x86_64 variant (which serves both Linux and Windows) on x86_64 +/// targets; otherwise emits the macOS or Linux AArch64 variant. Windows AArch64 is not +/// a supported target and panics. +/// +/// # Input +/// - AArch64: requested byte length in `x0`. +/// - x86_64: requested byte length in `rdi` (SysV first argument). +/// +/// # Output (an owned, kind-1 elephc string) +/// - AArch64: payload pointer in `x1`, length in `x2`. +/// - x86_64: payload pointer in `rax`, length in `rdx`. +pub fn emit_random_bytes(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_random_bytes_x86_64(emitter); + return; + } + + match emitter.platform { + Platform::MacOS => emit_random_bytes_macos_aarch64(emitter), + Platform::Linux => emit_random_bytes_linux_aarch64(emitter), + Platform::Windows => { + panic!("Windows ARM64 target is not yet supported (see issue #379)"); + } + } +} + +/// Emits the macOS AArch64 `__rt_random_bytes` helper backed by libc `arc4random_buf`. +/// +/// `arc4random_buf` fills the whole buffer in one call and never fails, so no retry loop +/// is needed. A length below 1 is rejected before allocation. +fn emit_random_bytes_macos_aarch64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: random_bytes ---"); + emitter.label_global("__rt_random_bytes"); + + // -- reject non-positive lengths before allocating -- + emitter.instruction("cmp x0, #1"); // check whether the requested length is below 1 + emitter.instruction("b.lt __rt_random_bytes_bad_length"); // reject 0 and negative lengths with a fatal error + + // -- set up a stack frame for the two libc calls -- + emitter.instruction("sub sp, sp, #32"); // reserve slots for the length, result pointer, and saved frame + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across the libc calls + emitter.instruction("add x29, sp, #16"); // establish a frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested length for the fill call and the return pair + + // -- allocate an owned string buffer of `length` bytes -- + emitter.instruction("bl __rt_heap_alloc"); // allocate length bytes of owned storage (size already in x0) + emitter.instruction("mov x9, #1"); // heap kind 1 = owned elephc string + emitter.instruction("str x9, [x0, #-8]"); // stamp the allocation as a string payload + emitter.instruction("str x0, [sp, #8]"); // save the result payload pointer for the return pair + + // -- fill the buffer with cryptographically secure bytes -- + emitter.instruction("ldr x1, [sp, #0]"); // arc4random_buf length argument (buffer already in x0) + emitter.bl_c("arc4random_buf"); // fill the buffer via libc CSPRNG; never fails + + // -- return the owned string pointer/length pair -- + emitter.instruction("ldr x1, [sp, #8]"); // return the result payload pointer + emitter.instruction("ldr x2, [sp, #0]"); // return the result length + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the helper stack frame + emitter.instruction("ret"); // return the CSPRNG string + + emit_aarch64_bad_length_fatal(emitter); +} + +/// Emits the Linux AArch64 `__rt_random_bytes` helper backed by the `getrandom` syscall. +/// +/// Keeps the buffer cursor and remaining count in callee-saved registers, reloads the +/// syscall number and arguments each iteration, advances on a partial fill, retries on +/// `-EINTR`, and aborts on any other negative return or if a sanity iteration cap is hit. +fn emit_random_bytes_linux_aarch64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: random_bytes ---"); + emitter.label_global("__rt_random_bytes"); + + // -- reject non-positive lengths before allocating -- + emitter.instruction("cmp x0, #1"); // check whether the requested length is below 1 + emitter.instruction("b.lt __rt_random_bytes_bad_length"); // reject 0 and negative lengths with a fatal error + + // -- set up a frame preserving the callee-saved loop registers (this helper returns) -- + emitter.instruction("sub sp, sp, #64"); // reserve the loop-register save area and frame slots + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #48"); // establish a frame pointer + emitter.instruction("stp x19, x20, [sp, #0]"); // preserve the caller's x19 (cursor) and x20 (remaining) + emitter.instruction("stp x21, x22, [sp, #16]"); // preserve the caller's x21 (base) and x22 (length) + emitter.instruction("str x23, [sp, #32]"); // preserve the caller's x23 (iteration cap) + + // -- allocate an owned string buffer of `length` bytes -- + emitter.instruction("str x0, [sp, #40]"); // spill length to the free frame slot across the alloc call (do not trust heap_alloc register discipline) + emitter.instruction("bl __rt_heap_alloc"); // allocate length bytes of owned storage (size already in x0) + emitter.instruction("ldr x22, [sp, #40]"); // reload the requested length after the alloc call + emitter.instruction("mov x9, #1"); // heap kind 1 = owned elephc string + emitter.instruction("str x9, [x0, #-8]"); // stamp the allocation as a string payload + emitter.instruction("mov x21, x0"); // result payload base pointer + emitter.instruction("mov x19, x0"); // buffer cursor = payload start + emitter.instruction("mov x20, x22"); // remaining bytes = length + emitter.instruction("mov x23, #256"); // sanity iteration cap for pathological partial fills + + // -- fill loop: getrandom may return fewer bytes or -EINTR -- + emitter.label("__rt_random_bytes_fill"); + emitter.instruction("cbz x20, __rt_random_bytes_done"); // done once every requested byte is filled + emitter.instruction("subs x23, x23, #1"); // consume one iteration from the sanity cap + emitter.instruction("b.lt __rt_random_bytes_source_fail"); // too many iterations → treat as a source failure + emitter.instruction("mov x0, x19"); // getrandom buffer = current cursor + emitter.instruction("mov x1, x20"); // getrandom length = remaining bytes + emitter.instruction("mov x2, #0"); // getrandom flags = 0 + emitter.instruction("mov x8, #278"); // Linux aarch64 getrandom syscall number + emitter.instruction("svc #0"); // request random bytes from the kernel CSPRNG + emitter.instruction("cmp x0, #0"); // did getrandom return a negative errno? + emitter.instruction("b.lt __rt_random_bytes_check_eintr"); // negative → distinguish EINTR from a hard failure + emitter.instruction("add x19, x19, x0"); // advance the cursor by the bytes filled + emitter.instruction("sub x20, x20, x0"); // decrement the remaining byte count + emitter.instruction("b __rt_random_bytes_fill"); // continue until the buffer is full + + // -- classify a negative getrandom return -- + emitter.label("__rt_random_bytes_check_eintr"); + emitter.instruction("cmn x0, #4"); // was the syscall interrupted (errno -EINTR)? + emitter.instruction("b.eq __rt_random_bytes_fill"); // retry on interruption + emitter.instruction("b __rt_random_bytes_source_fail"); // any other negative → fatal source failure + + // -- return the owned string pointer/length pair -- + emitter.label("__rt_random_bytes_done"); + emitter.instruction("mov x1, x21"); // return the result payload pointer + emitter.instruction("mov x2, x22"); // return the result length + emitter.instruction("ldp x19, x20, [sp, #0]"); // restore the caller's x19 and x20 + emitter.instruction("ldp x21, x22, [sp, #16]"); // restore the caller's x21 and x22 + emitter.instruction("ldr x23, [sp, #32]"); // restore the caller's x23 + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // release the helper stack frame + emitter.instruction("ret"); // return the CSPRNG string + + emit_aarch64_bad_length_fatal(emitter); + emit_aarch64_source_fail_fatal(emitter); +} + +/// Emits the shared AArch64 fatal path for a `random_bytes()` length below 1. +/// +/// Writes the invalid-length diagnostic to stderr and terminates the process. Uses the +/// platform-aware `syscall` helper so it lowers correctly on macOS and Linux. +fn emit_aarch64_bad_length_fatal(emitter: &mut Emitter) { + emitter.label("__rt_random_bytes_bad_length"); + emitter.instruction("mov x0, #2"); // fd = stderr for the invalid-length diagnostic + crate::codegen::abi::emit_symbol_address(emitter, "x1", "_random_bytes_length_msg"); + emitter.instruction(&format!("mov x2, #{}", RANDOM_BYTES_LENGTH_MSG.len())); // pass the exact diagnostic byte count + emitter.syscall(4); + emitter.instruction("mov x0, #1"); // exit code 1 for the invalid-length abort + emitter.syscall(1); +} + +/// Emits the AArch64 fatal path for an unavailable `random_bytes()` entropy source. +/// +/// Writes the CSPRNG-unavailable diagnostic to stderr and terminates the process rather +/// than returning weak or partially filled output. +fn emit_aarch64_source_fail_fatal(emitter: &mut Emitter) { + emitter.label("__rt_random_bytes_source_fail"); + emitter.instruction("mov x0, #2"); // fd = stderr for the entropy-source diagnostic + crate::codegen::abi::emit_symbol_address(emitter, "x1", "_random_bytes_source_msg"); + emitter.instruction(&format!("mov x2, #{}", RANDOM_BYTES_SOURCE_MSG.len())); // pass the exact diagnostic byte count + emitter.syscall(4); + emitter.instruction("mov x0, #1"); // exit code 1 for the entropy-source abort + emitter.syscall(1); +} + +/// Emits the x86_64 `__rt_random_bytes` helper, serving both Linux and Windows. +/// +/// Uses the Linux `getrandom` syscall form (`mov eax, 318` + `syscall`); on Windows the +/// syscall→shim transform rewrites that adjacent pair into `call __rt_sys_getrandom` +/// (BCryptGenRandom), which fills the whole buffer in one call. Loop state lives in +/// callee-saved registers (r12/r13) because the Windows shim clobbers rdi/rsi, and the +/// syscall arguments are reloaded every iteration. A length below 1, an iteration-cap +/// overflow, or any hard failure (a negative return other than `-EINTR`, including the +/// shim's `-1`) aborts with a fatal diagnostic and `exit(1)`. +fn emit_random_bytes_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: random_bytes ---"); + emitter.label_global("__rt_random_bytes"); + + // -- prologue: preserve callee-saved registers (this helper returns) -- + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable frame base + emitter.instruction("push rbx"); // preserve rbx (iteration cap counter) + emitter.instruction("push r12"); // preserve r12 (buffer cursor) + emitter.instruction("push r13"); // preserve r13 (remaining bytes) + emitter.instruction("push r14"); // preserve r14 (result base pointer) + emitter.instruction("push r15"); // preserve r15 (result length) + emitter.instruction("sub rsp, 8"); // realign the stack to 16 bytes before nested calls + + // -- reject non-positive lengths before allocating -- + emitter.instruction("test rdi, rdi"); // inspect the requested length + emitter.instruction("jle __rt_random_bytes_bad_length_x86"); // reject 0 and negative lengths with a fatal error + + // -- allocate an owned string buffer of `length` bytes -- + emitter.instruction("mov QWORD PTR [rsp], rdi"); // spill length to the reserved slot across the alloc call (do not trust heap_alloc register discipline) + emitter.instruction("mov rax, rdi"); // allocation size = length + emitter.instruction("call __rt_heap_alloc"); // allocate length bytes of owned storage (pointer in rax) + emitter.instruction("mov r15, QWORD PTR [rsp]"); // reload the requested length after the alloc call + emitter.instruction(&format!( + "mov r10, 0x{:x}", + crate::codegen_support::sentinels::x86_64_heap_kind_word(1) + )); // owned-string heap kind word with the x86_64 marker + emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the allocation as a string payload + emitter.instruction("mov r14, rax"); // save the result payload base pointer + emitter.instruction("mov r12, rax"); // buffer cursor = payload start + emitter.instruction("mov r13, r15"); // remaining bytes = length + emitter.instruction("mov ebx, 256"); // sanity iteration cap for pathological partial fills + + // -- fill loop: getrandom may return fewer bytes or -EINTR (Windows fills in one call) -- + emitter.label("__rt_random_bytes_fill_x86"); + emitter.instruction("test r13, r13"); // any bytes left to fill? + emitter.instruction("jz __rt_random_bytes_done_x86"); // done once the buffer is full + emitter.instruction("dec rbx"); // consume one iteration from the sanity cap + emitter.instruction("js __rt_random_bytes_source_fail_x86"); // too many iterations → treat as a source failure + emitter.instruction("mov rdi, r12"); // getrandom buffer = current cursor (reloaded; the shim clobbers rdi) + emitter.instruction("mov rsi, r13"); // getrandom length = remaining bytes (reloaded; the shim clobbers rsi) + emitter.instruction("xor edx, edx"); // getrandom flags = 0 + emitter.instruction("mov eax, 318"); // Linux x86_64 getrandom (Windows: → call __rt_sys_getrandom) + emitter.instruction("syscall"); // request random bytes from the CSPRNG + emitter.instruction("test rax, rax"); // did getrandom return a negative errno? + emitter.instruction("js __rt_random_bytes_check_eintr_x86"); // negative → distinguish EINTR from a hard failure + emitter.instruction("add r12, rax"); // advance the cursor by the bytes filled + emitter.instruction("sub r13, rax"); // decrement the remaining byte count + emitter.instruction("jmp __rt_random_bytes_fill_x86"); // continue until the buffer is full + + // -- classify a negative getrandom return -- + emitter.label("__rt_random_bytes_check_eintr_x86"); + emitter.instruction("cmp rax, -4"); // was the syscall interrupted (errno -EINTR)? + emitter.instruction("je __rt_random_bytes_fill_x86"); // retry on interruption + emitter.instruction("jmp __rt_random_bytes_source_fail_x86"); // any other negative (incl the shim's -1) → fatal + + // -- return the owned string pointer/length pair -- + emitter.label("__rt_random_bytes_done_x86"); + emitter.instruction("mov rax, r14"); // return the result payload pointer + emitter.instruction("mov rdx, r15"); // return the result length + emitter.instruction("add rsp, 8"); // release the alignment padding + emitter.instruction("pop r15"); // restore r15 + emitter.instruction("pop r14"); // restore r14 + emitter.instruction("pop r13"); // restore r13 + emitter.instruction("pop r12"); // restore r12 + emitter.instruction("pop rbx"); // restore rbx + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the CSPRNG string + + // -- fatal: length below 1 -- + emitter.label("__rt_random_bytes_bad_length_x86"); + crate::codegen::abi::emit_symbol_address(emitter, "rsi", "_random_bytes_length_msg"); + emitter.instruction(&format!("mov rdx, {}", RANDOM_BYTES_LENGTH_MSG.len())); // pass the exact diagnostic byte count + emitter.instruction("jmp __rt_random_bytes_fatal_write_x86"); // write the diagnostic and exit + + // -- fatal: entropy source unavailable -- + emitter.label("__rt_random_bytes_source_fail_x86"); + crate::codegen::abi::emit_symbol_address(emitter, "rsi", "_random_bytes_source_msg"); + emitter.instruction(&format!("mov rdx, {}", RANDOM_BYTES_SOURCE_MSG.len())); // pass the exact diagnostic byte count + emitter.instruction("jmp __rt_random_bytes_fatal_write_x86"); // write the diagnostic and exit + + // -- shared fatal writer: stderr message (rsi/rdx) then exit(1) -- + emitter.label("__rt_random_bytes_fatal_write_x86"); + emitter.instruction("mov edi, 2"); // fd = stderr for the fatal diagnostic + emitter.instruction("mov eax, 1"); // Linux x86_64 write (Windows: → call __rt_sys_write) + emitter.instruction("syscall"); // emit the fatal diagnostic before terminating + emitter.instruction("mov edi, 1"); // exit code 1 for the abort path + emitter.instruction("mov eax, 60"); // Linux x86_64 exit (Windows: → call __rt_sys_exit) + emitter.instruction("syscall"); // terminate the process after reporting the failure +} diff --git a/src/codegen_support/runtime/arrays/random_u32.rs b/src/codegen_support/runtime/arrays/random_u32.rs index 028e01fb6a..6eaf597433 100644 --- a/src/codegen_support/runtime/arrays/random_u32.rs +++ b/src/codegen_support/runtime/arrays/random_u32.rs @@ -69,7 +69,34 @@ pub fn emit_random_u32(emitter: &mut Emitter) { emitter.instruction("add sp, sp, #32"); // release the temporary stack frame emitter.instruction("ret"); // return the random uint32 } - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => { + emitter.instruction("sub sp, sp, #32"); // allocate stack space for the shim buffer and saved frame + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #16"); // set up a frame pointer for the fallback path + + emitter.instruction("mov x0, sp"); // buffer = stack scratch space + emitter.instruction("mov x1, #4"); // request 4 random bytes + emitter.instruction("mov x2, #0"); // flags = 0 + emitter.bl_c("__rt_sys_getrandom"); // ask the Win32 BCryptGenRandom shim for random bytes + emitter.instruction("cmp x0, #4"); // did the shim fill the full uint32 buffer? + emitter.instruction("b.eq __rt_random_u32_win_ok"); // yes — use the generated bytes + + // -- fallback: mix time(NULL) into a 32-bit value if getrandom is unavailable -- + emitter.instruction("mov x0, #0"); // pass NULL to time() to request only the return value + emitter.bl_c("time"); // get coarse wall-clock seconds from libc + emitter.instruction("mov x9, x0"); // preserve the low half for mixing + emitter.instruction("lsl x0, x0, #32"); // move the timestamp into the high half + emitter.instruction("eor x0, x0, x9"); // fold high and low halves together into one word + emitter.instruction("b __rt_random_u32_win_done"); // skip loading the shim buffer + + emitter.label("__rt_random_u32_win_ok"); + emitter.instruction("ldr w0, [sp, #0]"); // load the generated uint32 from the stack buffer + + emitter.label("__rt_random_u32_win_done"); + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the temporary stack frame + emitter.instruction("ret"); // return the random uint32 + } } } @@ -94,7 +121,7 @@ fn emit_random_u32_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("cmp rax, 4"); // check whether the kernel filled the full uint32 scratch buffer emitter.instruction("je __rt_random_u32_linux_ok_x86"); // use the kernel-generated bytes when getrandom succeeded emitter.instruction("xor edi, edi"); // pass NULL to time() so the fallback only returns the current timestamp - emitter.instruction("call time"); // fetch coarse wall-clock seconds as a deterministic fallback when getrandom is unavailable + emitter.emit_call_c("time"); // fetch coarse wall-clock seconds as a deterministic fallback when getrandom is unavailable emitter.instruction("mov rcx, rax"); // preserve the low half of the fallback timestamp before mixing it into one uint32 word emitter.instruction("shl rax, 32"); // move the fallback timestamp into the high half so both halves contribute to the mixed fallback value emitter.instruction("xor rax, rcx"); // fold the high and low halves together into one pseudo-randomish uint32 candidate diff --git a/src/codegen_support/runtime/arrays/random_uniform.rs b/src/codegen_support/runtime/arrays/random_uniform.rs index 39bbf20d28..19f2c64322 100644 --- a/src/codegen_support/runtime/arrays/random_uniform.rs +++ b/src/codegen_support/runtime/arrays/random_uniform.rs @@ -7,6 +7,13 @@ //! //! Key details: //! - Random helpers produce bounded integer values used by array_rand and shuffle without modulo bias where possible. +//! - `__rt_random_uniform` is a 32-bit sampler: its bound lives in `edi`/`w0`. It +//! backs array_rand and shuffle, whose bound is an element count. `random_int`, +//! `rand` and `mt_rand` accept the full PHP integer range, so they go through +//! `__rt_random_uniform64`, which takes an *inclusive* width and delegates to the +//! 32-bit sampler whenever the range fits — mirroring php-src's +//! `php_random_range()` dispatch between `php_random_range32`/`php_random_range64` +//! (ext/random/random.c). use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; @@ -110,3 +117,149 @@ fn emit_random_uniform_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the caller frame pointer after the x86_64 rejection-sampling helper completes emitter.instruction("ret"); // return the unbiased random value in eax } + +/// Emits the `__rt_random_u64` and `__rt_random_uniform64` runtime helpers. +/// +/// `__rt_random_uniform64` takes an **inclusive** range width `umax` and returns a +/// uniform value in `[0, umax]`. The inclusive convention is what makes the full +/// PHP integer range representable: `random_int(PHP_INT_MIN, PHP_INT_MAX)` has +/// `max - min + 1 == 2^64`, which is zero in 64 bits, whereas `max - min` is +/// `UINT64_MAX` and needs no wider arithmetic. +/// +/// This mirrors php-src `php_random_range()` (ext/random/random.c), which selects +/// `php_random_range32` when `umax <= UINT32_MAX` and `php_random_range64` +/// otherwise, and which special-cases `umax == UINT64_MAX` before incrementing. +/// +/// # ABI +/// - AArch64: `umax` in `x0`, result in `x0` +/// - x86_64: `umax` in `rdi`, result in `rax` +pub fn emit_random_uniform64(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_random_uniform64_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: random_u64 ---"); + emitter.label_global("__rt_random_u64"); + emitter.instruction("sub sp, sp, #32"); // reserve a frame so the two nested u32 draws can preserve state + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across the helper calls + emitter.instruction("add x29, sp, #16"); // establish a frame pointer for the composed draw + emitter.instruction("bl __rt_random_u32"); // draw the low half from the platform CSPRNG + emitter.instruction("str w0, [sp, #0]"); // stash the low half while the second draw runs + emitter.instruction("bl __rt_random_u32"); // draw the high half from the platform CSPRNG + emitter.instruction("ldr w9, [sp, #0]"); // reload the low half as a zero-extended uint32 + emitter.instruction("lsl x0, x0, #32"); // move the second draw into the high 32 bits + emitter.instruction("orr x0, x0, x9"); // compose both independent draws into one uint64 + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the temporary stack frame + emitter.instruction("ret"); // return the composed random uint64 + + emitter.blank(); + emitter.comment("--- runtime: random_uniform64 ---"); + emitter.label_global("__rt_random_uniform64"); + emitter.instruction("sub sp, sp, #32"); // reserve slots for the element count and rejection limit + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #16"); // establish a frame pointer for the sampling paths + emitter.instruction("cmn x0, #1"); // is the inclusive width UINT64_MAX (compare against -1)? + emitter.instruction("b.ne __rt_random_uniform64_bounded"); // no — the width can be incremented without wrapping + emitter.instruction("bl __rt_random_u64"); // the whole uint64 space is admissible, so draw once + emitter.instruction("b __rt_random_uniform64_done"); // and return it unreduced + + emitter.label("__rt_random_uniform64_bounded"); + emitter.instruction("add x0, x0, #1"); // convert the inclusive width into an exclusive element count + emitter.instruction("lsr x9, x0, #32"); // does the count need more than 32 bits? + emitter.instruction("cbnz x9, __rt_random_uniform64_wide"); // yes — sample across the full uint64 space + emitter.instruction("bl __rt_random_uniform"); // no — reuse the 32-bit rejection sampler with the count in w0 + emitter.instruction("b __rt_random_uniform64_done"); // its result is already in [0, count) + + emitter.label("__rt_random_uniform64_wide"); + emitter.instruction("str x0, [sp, #0]"); // preserve the element count across the sampling loop + emitter.instruction("mov x10, #-1"); // load UINT64_MAX as the rejection-limit dividend + emitter.instruction("udiv x11, x10, x0"); // quotient = UINT64_MAX / count + emitter.instruction("msub x11, x11, x0, x10"); // remainder = UINT64_MAX % count + emitter.instruction("sub x11, x10, x11"); // UINT64_MAX - (UINT64_MAX % count) + emitter.instruction("sub x11, x11, #1"); // limit = the largest value that reduces without bias + emitter.instruction("str x11, [sp, #8]"); // preserve the rejection limit across the sampling loop + + emitter.label("__rt_random_uniform64_loop"); + emitter.instruction("bl __rt_random_u64"); // draw a fresh uint64 candidate + emitter.instruction("ldr x11, [sp, #8]"); // reload the rejection limit + emitter.instruction("cmp x0, x11"); // candidate inside the unbiased prefix? + emitter.instruction("b.hi __rt_random_uniform64_loop"); // no — discard and resample + emitter.instruction("ldr x9, [sp, #0]"); // reload the element count + emitter.instruction("udiv x10, x0, x9"); // quotient = candidate / count + emitter.instruction("msub x0, x10, x9, x0"); // remainder = candidate % count + + emitter.label("__rt_random_uniform64_done"); + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the temporary stack frame + emitter.instruction("ret"); // return the unbiased value in [0, umax] +} + +/// Emits the x86_64 `__rt_random_u64` and `__rt_random_uniform64` helpers. +/// +/// Same algorithm as the AArch64 path. `umax` arrives in `rdi` and the result is +/// returned in `rax`; the element count and rejection limit are stack-allocated at +/// `[rbp - 8]` and `[rbp - 16]` so they survive the nested draw calls. +fn emit_random_uniform64_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: random_u64 ---"); + emitter.label_global("__rt_random_u64"); + emitter.instruction("push rbp"); // preserve the caller frame pointer before composing two draws + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the stashed low half + emitter.instruction("sub rsp, 16"); // reserve aligned scratch space across the nested helper calls + emitter.instruction("call __rt_random_u32"); // draw the low half from the platform CSPRNG + emitter.instruction("mov DWORD PTR [rbp - 4], eax"); // stash the low half while the second draw runs + emitter.instruction("call __rt_random_u32"); // draw the high half from the platform CSPRNG + emitter.instruction("mov eax, eax"); // zero-extend the second draw into the full 64-bit register + emitter.instruction("shl rax, 32"); // move the second draw into the high 32 bits + emitter.instruction("mov ecx, DWORD PTR [rbp - 4]"); // reload the low half as a zero-extended uint32 + emitter.instruction("or rax, rcx"); // compose both independent draws into one uint64 + emitter.instruction("add rsp, 16"); // release the scratch space before returning + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the composed random uint64 + + emitter.blank(); + emitter.comment("--- runtime: random_uniform64 ---"); + emitter.label_global("__rt_random_uniform64"); + emitter.instruction("push rbp"); // preserve the caller frame pointer before reserving sampling scratch + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the count and rejection limit + emitter.instruction("sub rsp, 32"); // reserve aligned space for the element count and rejection limit + emitter.instruction("cmp rdi, -1"); // is the inclusive width UINT64_MAX? + emitter.instruction("jne __rt_random_uniform64_bounded_x86"); // no — the width can be incremented without wrapping + emitter.instruction("call __rt_random_u64"); // the whole uint64 space is admissible, so draw once + emitter.instruction("jmp __rt_random_uniform64_done_x86"); // and return it unreduced + + emitter.label("__rt_random_uniform64_bounded_x86"); + emitter.instruction("add rdi, 1"); // convert the inclusive width into an exclusive element count + emitter.instruction("mov rcx, rdi"); // copy the count so the high half can be tested + emitter.instruction("shr rcx, 32"); // does the count need more than 32 bits? + emitter.instruction("jnz __rt_random_uniform64_wide_x86"); // yes — sample across the full uint64 space + emitter.instruction("call __rt_random_uniform"); // no — reuse the 32-bit rejection sampler with the count in edi + emitter.instruction("mov eax, eax"); // zero-extend its uint32 result into the 64-bit return register + emitter.instruction("jmp __rt_random_uniform64_done_x86"); // its result is already in [0, count) + + emitter.label("__rt_random_uniform64_wide_x86"); + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the element count across the sampling loop + emitter.instruction("mov rax, -1"); // load UINT64_MAX as the rejection-limit dividend + emitter.instruction("xor edx, edx"); // clear the high dividend half before the 64-bit division + emitter.instruction("div rdi"); // rdx = UINT64_MAX % count + emitter.instruction("mov rax, -1"); // reload UINT64_MAX to subtract the remainder from it + emitter.instruction("sub rax, rdx"); // UINT64_MAX - (UINT64_MAX % count) + emitter.instruction("sub rax, 1"); // limit = the largest value that reduces without bias + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // preserve the rejection limit across the sampling loop + + emitter.label("__rt_random_uniform64_loop_x86"); + emitter.instruction("call __rt_random_u64"); // draw a fresh uint64 candidate + emitter.instruction("cmp rax, QWORD PTR [rbp - 16]"); // candidate inside the unbiased prefix? + emitter.instruction("ja __rt_random_uniform64_loop_x86"); // no — discard and resample + emitter.instruction("xor edx, edx"); // clear the high dividend half before reducing modulo the count + emitter.instruction("div QWORD PTR [rbp - 8]"); // rdx = candidate % count + emitter.instruction("mov rax, rdx"); // return the unbiased remainder + + emitter.label("__rt_random_uniform64_done_x86"); + emitter.instruction("add rsp, 32"); // release the sampling scratch slots before returning + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the unbiased value in [0, umax] +} diff --git a/src/codegen_support/runtime/arrays/shuffle.rs b/src/codegen_support/runtime/arrays/shuffle.rs index 9ee68d57eb..124b778e7c 100644 --- a/src/codegen_support/runtime/arrays/shuffle.rs +++ b/src/codegen_support/runtime/arrays/shuffle.rs @@ -90,6 +90,7 @@ fn emit_shuffle_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("lea rdi, [r10 + 1]"); // pass the exclusive upper bound i + 1 to the uniform random helper emitter.instruction("call __rt_random_uniform"); // draw a random slot index j in the inclusive range [0, i] emitter.instruction("mov r11, rax"); // preserve the sampled Fisher-Yates partner index before reloading the array base pointer + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload cursor i after the random helper clobbered caller-saved r10 emitter.instruction("mov r8, QWORD PTR [rbp - 8]"); // reload the indexed-array pointer after the random helper clobbered caller-saved registers emitter.instruction("lea r9, [r8 + 24]"); // compute the indexed-array payload base pointer so the swap can address element slots directly emitter.instruction("mov rax, QWORD PTR [r9 + r10 * 8]"); // load the current Fisher-Yates tail element that will be swapped toward the sampled position diff --git a/src/codegen_support/runtime/arrays/usort.rs b/src/codegen_support/runtime/arrays/usort.rs index b98f6f7e70..0c42e86ea2 100644 --- a/src/codegen_support/runtime/arrays/usort.rs +++ b/src/codegen_support/runtime/arrays/usort.rs @@ -117,7 +117,7 @@ fn emit_usort_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("push r13"); // preserve the indexed-array pointer across nested comparator calls emitter.instruction("push r14"); // preserve the indexed-array length across nested comparator calls emitter.instruction("push r15"); // preserve the swapped flag across nested comparator calls - emitter.instruction("sub rsp, 16"); // reserve a local slot for the optional callback environment pointer + emitter.instruction("sub rsp, 24"); // reserve the callback environment slot and keep nested callback calls 16-byte aligned emitter.instruction("mov r12, rdi"); // preserve the comparator callback address in a callee-saved register for the whole bubble-sort pass emitter.instruction("mov r13, rsi"); // preserve the indexed-array pointer in a callee-saved register for the whole bubble-sort pass emitter.instruction("mov QWORD PTR [rbp - 48], rdx"); // save optional callback environment pointer for captured comparator wrappers @@ -142,7 +142,7 @@ fn emit_usort_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("je __rt_usort_call_linux_x86_64"); // keep legacy two-argument comparator ABI when no environment is present emitter.instruction("mov rdx, QWORD PTR [rbp - 48]"); // pass capture environment after the compared pair emitter.label("__rt_usort_call_linux_x86_64"); - emitter.instruction("call r12"); // invoke the user comparator callback on the current adjacent indexed-array pair + emitter.emit_platform_callback_call("r12", 3); emitter.instruction("cmp rax, 0"); // did the comparator report that the current adjacent pair is already ordered? emitter.instruction("jle __rt_usort_noswap_linux_x86_64"); // skip the swap path when the comparator says the left element should stay before the right element emitter.instruction("lea r10, [r13 + 24]"); // reload the indexed-array payload base after the comparator call clobbered caller-saved registers @@ -162,7 +162,7 @@ fn emit_usort_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jnz __rt_usort_outer_linux_x86_64"); // repeat another bubble-sort pass while at least one adjacent pair was swapped emitter.label("__rt_usort_done_linux_x86_64"); - emitter.instruction("add rsp, 16"); // release the comparator environment slot before restoring callee-saved registers + emitter.instruction("add rsp, 24"); // release the aligned comparator environment area before restoring callee-saved registers emitter.instruction("pop r15"); // restore the saved swapped-flag register after the x86_64 usort() helper finishes emitter.instruction("pop r14"); // restore the saved indexed-array length register after the x86_64 usort() helper finishes emitter.instruction("pop r13"); // restore the saved indexed-array pointer register after the x86_64 usort() helper finishes diff --git a/src/codegen_support/runtime/arrays/value_error.rs b/src/codegen_support/runtime/arrays/value_error.rs index 147c5a5740..f3e7c31724 100644 --- a/src/codegen_support/runtime/arrays/value_error.rs +++ b/src/codegen_support/runtime/arrays/value_error.rs @@ -50,22 +50,8 @@ pub(in crate::codegen_support::runtime) fn emit_throw_value_error_x86_64( 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, 56"); // request Throwable payload storage (message/code/previous) - emitter.instruction("call __rt_heap_alloc"); // allocate the ValueError object payload - emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(6))); // stamp the canonical x86_64 heap-kind word (magic + kind 6 throwable) - 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 - emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null - 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 + abi::emit_load_symbol_to_reg(emitter, "rdi", "_spl_value_error_class_id", 0); // pass ValueError's runtime class id + abi::emit_symbol_address(emitter, "rsi", message_symbol); // pass the static ValueError message pointer + emitter.instruction(&format!("mov rdx, {}", message_len)); // pass the static ValueError message length + emitter.instruction("jmp __rt_throw_static_exception"); // allocate and publish through an independent unwind frame } diff --git a/src/codegen_support/runtime/data/fixed.rs b/src/codegen_support/runtime/data/fixed.rs index 051b9551fa..d47f4e89af 100644 --- a/src/codegen_support/runtime/data/fixed.rs +++ b/src/codegen_support/runtime/data/fixed.rs @@ -6,36 +6,37 @@ //! - `crate::codegen_support::runtime::data::emit_runtime_data_fixed()`. //! //! Key details: -//! - Fixed symbols are cached across compilations, so only target-independent runtime data belongs here. +//! - Fixed symbols are cached per target; target-specific callable availability +//! and symbol spelling must use the target included in that cache key. use super::{ DIRNAME_LEVELS_MSG, HASH_HMAC_UNKNOWN_ALGO_MSG, HASH_INIT_UNKNOWN_ALGO_MSG, - HASH_UNKNOWN_ALGO_MSG, MB_STRLEN_UNKNOWN_ENCODING_MSG, + ESCAPE_SHELL_ARG_INPUT_LENGTH_MSG, ESCAPE_SHELL_ARG_NUL_MSG, ESCAPE_SHELL_ARG_OUTPUT_LENGTH_MSG, + ESCAPE_SHELL_CMD_INPUT_LENGTH_MSG, ESCAPE_SHELL_CMD_NUL_MSG, ESCAPE_SHELL_CMD_OUTPUT_LENGTH_MSG, HASH_UNKNOWN_ALGO_MSG, + MB_STRLEN_UNKNOWN_ENCODING_MSG, OB_CLOSURE_INVOKE_NAME, OB_DEFAULT_HANDLER_NAME, OB_FATAL_IN_HANDLER, OB_NTC_CREATE_FAIL, OB_NTC_G_CLEAN, OB_NTC_G_END_CLEAN, OB_NTC_G_END_FLUSH, OB_NTC_G_FLUSH, OB_NTC_G_GET_CLEAN, OB_NTC_G_GET_FLUSH, OB_NTC_NO_CLEAN, OB_NTC_NO_END_CLEAN, OB_NTC_NO_END_FLUSH, OB_NTC_NO_FLUSH, OB_NTC_NO_GET_FLUSH, OB_WARN_BAD_CALLBACK_GENERIC, OB_WARN_BAD_CALLBACK_PREFIX, OB_WARN_BAD_CALLBACK_SUFFIX, - PHP_UNAME_MODE_LEN_MSG, PHP_UNAME_MODE_VALUE_MSG, STR_REPEAT_TIMES_MSG, + PHP_UNAME_MODE_LEN_MSG, PHP_UNAME_MODE_VALUE_MSG, RANDOM_BYTES_LENGTH_MSG, + RANDOM_BYTES_SOURCE_MSG, STR_REPEAT_TIMES_MSG, TEMPNAM_FALLBACK_NOTICE, }; use super::super::system; -use crate::codegen_support::platform::Target; -use crate::types::checker::builtins::supported_builtin_function_names; +use crate::codegen_support::platform::{Platform, Target}; /// Emit the fixed runtime `.data` section as assembly text. -/// Cached across compilations because it contains only target-independent -/// runtime data: heap globals, concat buffers, exception/fiber state, -/// JSON/SPL error messages, base64 tables, PCRE regex patterns, and -/// lookup tables for builtins, file types, and `pathinfo` keys. +/// Cached across compilations per target because it contains heap globals, +/// concat buffers, exception/fiber state, JSON/SPL error messages, base64 +/// tables, PCRE regex patterns, and lookup tables for builtins, file types, +/// and `pathinfo` keys. /// /// `heap_size` is the maximum heap bytes requested by the user program; /// it is baked into `_heap_max` to enforce the heap limit at runtime. /// -/// `target` is needed only for the one symbol the `--web` bridge references -/// (`elephc_web_capture`): it must carry the platform's C-ABI mangling so the -/// runtime's `.comm`, the runtime's load, and the Rust bridge's `extern "C"` -/// all name the same symbol (`_elephc_web_capture` on macOS, `elephc_web_capture` -/// on Linux). The cache key already includes the target, so this stays cache-safe. +/// `target` selects the PHP-visible builtin callable table and the C-ABI +/// spelling of `elephc_web_capture`. The cache key already includes the target, +/// so both target-specific data surfaces remain cache-safe. pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> String { let mut out = String::new(); out.push_str(".data\n"); @@ -98,6 +99,9 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin out.push_str(&format!( ".globl _ob_handler_name\n_ob_handler_name:\n .ascii {OB_DEFAULT_HANDLER_NAME:?}\n" )); + out.push_str(&format!( + ".globl _tempnam_fallback_notice\n_tempnam_fallback_notice:\n .ascii {TEMPNAM_FALLBACK_NOTICE:?}\n" + )); for (sym, key) in [ ("_ob_k_name", "name"), ("_ob_k_type", "type"), @@ -161,6 +165,43 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin out.push_str(".globl _php_tz_utc\n"); out.push_str("_php_tz_utc:\n"); out.push_str(" .ascii \"UTC\"\n"); + // _elephc_tz_offset_fn: indirect pointer to elephc_tz_offset, published only + // at a windows date()/mktime()/strtotime() call site so the windows-only + // __rt_sys_localtime/__rt_sys_mktime shims (codegen_support/runtime/win32/ + // shims_time.rs) can call through it to resolve the active default + // timezone's IANA offset/DST. Programs that never reach a publishing call + // site leave the slot null, and the shims fall back to raw msvcrt + // localtime()/mktime() — so only windows programs that use date/time + // builtins pull in -lelephc_tz. The slot is emitted windows-only (like + // _win_tz_tm_buf below); no non-windows code references it. + if target.platform == Platform::Windows { + out.push_str(".comm _elephc_tz_offset_fn, 8, 3\n"); + out.push_str(".comm _elephc_tz_abbreviation_fn, 8, 3\n"); + } + // _win_tz_tm_buf: windows-only synthesized `struct tm` (glibc-layout: the 9 + // standard int fields at +0..+36, `tm_gmtoff` (i64) at +40, `tm_zone` + // (char*) at +48) returned by __rt_sys_localtime when the offset bridge + // resolves the active zone. Needed because MinGW's own `struct tm` (see + // its ) is only the 9 plain ints — msvcrt's static localtime()/ + // gmtime() buffer has no room for tm_gmtoff/tm_zone, so those fields must + // live in elephc's own storage, not msvcrt's, whenever the runtime's + // date() formatter reads them (the 'Z'/'O'/'P'/'I' specifiers). + if target.platform == Platform::Windows { + out.push_str(".comm _win_tz_tm_buf, 64, 3\n"); + out.push_str(".comm _win_select_read_handles, 512, 3\n"); + out.push_str(".comm _win_select_write_handles, 512, 3\n"); + out.push_str(".comm _win_select_except_handles, 512, 3\n"); + // 64 records of (active, descriptor/SOCKET, Linux status flags). The + // Win32 fcntl shim uses this to preserve F_GETFL/F_SETFL semantics for + // CRT descriptors and opaque Winsock SOCKET values. + out.push_str(".comm _win_fd_status_records, 1536, 3\n"); + // Process-wide impersonation-token cache for the php-parity ACL check + // behind is_readable()/is_writable() (win32/shims_fs.rs, + // __rt_win_acl_token_get). Tri-state: 0 = the token dance has never + // run, -1 = it ran and failed permanently (never retry; stay on the + // GetFileAttributesW answer), anything else = the cached token. + out.push_str(".comm _win_acl_token, 8, 3\n"); + } // getdate() associative-array key strings (read by __rt_getdate). for (sym, key) in [ ("_gd_k_seconds", "seconds"), @@ -193,6 +234,14 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin out.push_str(".comm _fiber_main_saved_exc, 8, 3\n"); out.push_str(".comm _fiber_main_saved_call_frame, 8, 3\n"); out.push_str(".comm _elephc_eval_dynamic_object_destruct_fn, 8, 3\n"); + // Windows-only: saved main-thread TEB stack metadata (NT_TIB + // StackBase/StackLimit and TEB DeallocationStack), snapshotted when leaving + // the main thread and restored when a fiber switches back. + if target.platform == Platform::Windows { + out.push_str(".comm _fiber_main_saved_stack_base, 8, 3\n"); + out.push_str(".comm _fiber_main_saved_stack_limit, 8, 3\n"); + out.push_str(".comm _fiber_main_saved_deallocation_stack, 8, 3\n"); + } out.push_str(".comm _rt_diag_suppression, 8, 3\n"); // elephc_web_capture: per-request output-capture mode flag read by // __rt_stdout_write. Zero (the default) routes echo output to the plain @@ -207,6 +256,21 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin )); out.push_str(&format!(".comm _heap_buf, {}, 3\n", heap_size)); out.push_str(".comm _heap_off, 8, 3\n"); + // proc_open() retains the exact `$pipes` container in a linked registry + // until proc_close() marks/closes every associated pipe before waiting. + // Keeping this root outside PHP locals is what makes a pipe-full child + // unable to deadlock a parent that calls proc_close() without reading it. + out.push_str(".comm _proc_pipe_registry_head, 8, 3\n"); + out.push_str(".comm _proc_status_registry_head, 8, 3\n"); + out.push_str(".globl _proc_status_k_command\n_proc_status_k_command:\n .ascii \"command\"\n"); + out.push_str(".globl _proc_status_k_pid\n_proc_status_k_pid:\n .ascii \"pid\"\n"); + out.push_str(".globl _proc_status_k_cached\n_proc_status_k_cached:\n .ascii \"cached\"\n"); + out.push_str(".globl _proc_status_k_running\n_proc_status_k_running:\n .ascii \"running\"\n"); + out.push_str(".globl _proc_status_k_signaled\n_proc_status_k_signaled:\n .ascii \"signaled\"\n"); + out.push_str(".globl _proc_status_k_stopped\n_proc_status_k_stopped:\n .ascii \"stopped\"\n"); + out.push_str(".globl _proc_status_k_exitcode\n_proc_status_k_exitcode:\n .ascii \"exitcode\"\n"); + out.push_str(".globl _proc_status_k_termsig\n_proc_status_k_termsig:\n .ascii \"termsig\"\n"); + out.push_str(".globl _proc_status_k_stopsig\n_proc_status_k_stopsig:\n .ascii \"stopsig\"\n"); out.push_str(".comm _heap_free_list, 8, 3\n"); out.push_str(".comm _heap_small_bins, 32, 3\n"); out.push_str(".comm _heap_debug_enabled, 8, 3\n"); @@ -244,6 +308,14 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin ".globl _str_repeat_times_msg\n_str_repeat_times_msg:\n .ascii {:?}\n", STR_REPEAT_TIMES_MSG )); + out.push_str(&format!( + ".globl _random_bytes_length_msg\n_random_bytes_length_msg:\n .ascii {:?}\n", + RANDOM_BYTES_LENGTH_MSG + )); + out.push_str(&format!( + ".globl _random_bytes_source_msg\n_random_bytes_source_msg:\n .ascii {:?}\n", + RANDOM_BYTES_SOURCE_MSG + )); out.push_str(&format!( ".globl _hash_unknown_algo_msg\n_hash_unknown_algo_msg:\n .ascii {:?}\n", HASH_UNKNOWN_ALGO_MSG @@ -260,6 +332,22 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin ".globl _mb_strlen_unknown_encoding_msg\n_mb_strlen_unknown_encoding_msg:\n .ascii {:?}\n", MB_STRLEN_UNKNOWN_ENCODING_MSG )); + out.push_str(&format!( + ".globl _escapeshellarg_nul_msg\n_escapeshellarg_nul_msg:\n .ascii {:?}\n", + ESCAPE_SHELL_ARG_NUL_MSG + )); + out.push_str(&format!( + ".globl _escapeshellcmd_nul_msg\n_escapeshellcmd_nul_msg:\n .ascii {:?}\n", + ESCAPE_SHELL_CMD_NUL_MSG + )); + for (label, message) in [ + ("_escapeshellarg_input_length_msg", ESCAPE_SHELL_ARG_INPUT_LENGTH_MSG), + ("_escapeshellarg_output_length_msg", ESCAPE_SHELL_ARG_OUTPUT_LENGTH_MSG), + ("_escapeshellcmd_input_length_msg", ESCAPE_SHELL_CMD_INPUT_LENGTH_MSG), + ("_escapeshellcmd_output_length_msg", ESCAPE_SHELL_CMD_OUTPUT_LENGTH_MSG), + ] { + out.push_str(&format!(".globl {label}\n{label}:\n .ascii {message:?}\n")); + } out.push_str(".globl _mb_strlen_utf8_name\n_mb_strlen_utf8_name:\n .asciz \"UTF-8\"\n"); out.push_str(".globl _mb_strlen_utf8_alias\n_mb_strlen_utf8_alias:\n .asciz \"UTF8\"\n"); out.push_str(".globl _mb_strlen_utf32le_name\n_mb_strlen_utf32le_name:\n .asciz \"UTF-32LE\"\n"); @@ -364,7 +452,7 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin out.push_str(".globl _fiber_msg_suspend_outside\n_fiber_msg_suspend_outside:\n .ascii \"Cannot suspend outside of a fiber\"\n"); out.push_str(".globl _fiber_msg_unsupported_callable\n_fiber_msg_unsupported_callable:\n .ascii \"Fiber callable is not supported by this compiler\"\n"); out.push_str(".globl _fiber_msg_stack_alloc_failed\n_fiber_msg_stack_alloc_failed:\n .ascii \"Cannot allocate fiber stack\"\n"); - out.push_str(&emit_builtin_callable_data()); + out.push_str(&emit_builtin_callable_data(target.platform)); out.push_str(".comm _gc_allocs, 8, 3\n"); out.push_str(".comm _gc_frees, 8, 3\n"); out.push_str(".comm _gc_live, 8, 3\n"); @@ -372,8 +460,20 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin out.push_str(".comm _cstr_buf, 4096, 3\n"); out.push_str(".comm _cstr_buf2, 4096, 3\n"); out.push_str(".comm _eof_flags, 256, 3\n"); + // Windows SOCKET values are pointer-sized opaque handles, not compact CRT + // descriptors. These two tables map every Windows stream handle to one of + // the 256 safe indexes shared by EOF and stream-filter state. + out.push_str(".comm _win_stream_slot_handles, 2048, 3\n"); + out.push_str(".comm _win_stream_slot_used, 256, 3\n"); + out.push_str(".comm _win_stream_timed_out, 256, 3\n"); out.push_str(".comm _popen_files, 2048, 3\n"); out.push_str(".comm _dir_handles, 2048, 3\n"); + // Per-process tables for proc_open (256 fds × 8B each). _proc_pids stores the + // child PID keyed by the parent-side pipe fd; _proc_child_fds stores the child + // process handle (Windows) / pid mirror used by proc_close to reap the child. + // C1a declares the storage; C1b/C1c populate it from the real runtime helpers. + out.push_str(".comm _proc_pids, 2048, 3\n"); + out.push_str(".comm _proc_child_fds, 2048, 3\n"); // Per-fd glob:// state pointers (256 fds × 8B). Each slot is a pointer to // a heap-allocated glob_state struct (pathv ptr + pathc + index + the // libc glob_t whose lifetime globfree() needs at closedir time). The @@ -421,34 +521,36 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin // actually open https URLs reference elephc-tls (and pull in -lelephc_tls // at link time); other programs keep the runtime libc-only. out.push_str(".comm _elephc_tls_connect_fn, 8, 3\n"); - // _elephc_tls_connect_insecure_fn: same shape as _elephc_tls_connect_fn - // but dispatched when the caller has set ssl.verify_peer = false on the - // stream context. The runtime picks one over the other at https_open - // time so non-TLS programs still don't link elephc-tls. + // _elephc_tls_connect_with_options_fn: policy-aware v3 bridge entry. It + // consumes one pointer to the stable 88-byte ElephcTlsClientOptions ABI. + out.push_str(".comm _elephc_tls_connect_with_options_fn, 8, 3\n"); + // The insecure/cafile/capath/peer-name slots preserve the historical bridge + // exports for compatibility. New context-aware clients combine those + // settings through _elephc_tls_connect_with_options_fn instead. out.push_str(".comm _elephc_tls_connect_insecure_fn, 8, 3\n"); - // _elephc_tls_connect_cafile_fn: dispatched when the caller has set - // ssl.cafile on the stream context. Same late-binding pattern; takes two - // extra args (cafile path ptr/len) that the secure/insecure variants ignore. out.push_str(".comm _elephc_tls_connect_cafile_fn, 8, 3\n"); - // _elephc_tls_connect_capath_fn / _peer_name_fn: dispatched for ssl.capath - // (a directory of CA certs) and ssl.peer_name (verify the cert for a name - // other than the connection host). Same late-binding/extra-args pattern. out.push_str(".comm _elephc_tls_connect_capath_fn, 8, 3\n"); out.push_str(".comm _elephc_tls_connect_peer_name_fn, 8, 3\n"); out.push_str(".comm _elephc_tls_write_fn, 8, 3\n"); out.push_str(".comm _elephc_tls_read_fn, 8, 3\n"); out.push_str(".comm _elephc_tls_close_fn, 8, 3\n"); + // _elephc_tls_handshake_fn advances a rustls session after it has been + // attached to an existing socket. It returns 1=complete, 0=would-block, + // -1=terminal failure so stream_socket_enable_crypto can preserve PHP's + // tri-state contract. + out.push_str(".comm _elephc_tls_handshake_fn, 8, 3\n"); // _elephc_tls_attach_fd_fn: indirect pointer to elephc_tls_attach_fd, // used by stream_socket_enable_crypto to promote an existing TCP fd to // a TLS session without re-establishing the TCP connection. Same // late-binding pattern as the other tls fn slots so non-TLS programs // do not pull in elephc-tls at link time. out.push_str(".comm _elephc_tls_attach_fd_fn, 8, 3\n"); - // _elephc_tls_attach_fd_client_cert_fn / _elephc_tls_connect_client_cert_fn: - // mutual-TLS variants dispatched when the stream context carries both - // ssl.local_cert and ssl.local_pk. The attach variant is used by - // stream_socket_enable_crypto; both take the extra cert/key path ptr/len - // pairs that the non-client-cert variants ignore. Same late-binding pattern. + // _elephc_tls_attach_fd_with_options_fn: policy-aware attach counterpart + // used when an existing socket is promoted to TLS. + out.push_str(".comm _elephc_tls_attach_fd_with_options_fn, 8, 3\n"); + // The client-cert-specific attach/connect slots also preserve historical + // exports. New stream-context consumers combine cert/key paths with the + // remaining TLS options through the uniform options entries above. out.push_str(".comm _elephc_tls_attach_fd_client_cert_fn, 8, 3\n"); out.push_str(".comm _elephc_tls_connect_client_cert_fn, 8, 3\n"); // _elephc_crypto_hash_fn: indirect pointer to elephc_crypto_hash, published @@ -527,11 +629,11 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin // _phar_list_len: output-length scratch written by elephc_phar_list_entries // and consumed immediately while expanding serialized names into an array. out.push_str(".p2align 3\n.globl _phar_list_len\n_phar_list_len:\n .quad 0\n"); - // _tls_sessions: per-fd TLS handle (i64 returned by - // elephc_tls_attach_fd or 0 when the fd is plain TCP). Indexed by raw - // fd up to 256; the runtime fread/fwrite/fclose paths consult this - // table and route through the elephc-tls helpers when an entry is - // non-zero, falling back to read/write/close syscalls otherwise. + // _tls_session_fds/_tls_sessions: bounded associative pairs mapping a full-width + // raw descriptor or Winsock SOCKET to the i64 handle returned by + // elephc_tls_attach_fd. Session zero marks an unused slot. Runtime stream paths + // access the pairs only through __rt_tls_session_{get,set,clear}. + out.push_str(".comm _tls_session_fds, 2048, 3\n"); out.push_str(".comm _tls_sessions, 2048, 3\n"); // _stream_chunk_size: per-fd read/write chunk size set by // stream_set_chunk_size, indexed by raw fd up to 256 (8 bytes each). A zero @@ -539,10 +641,10 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin // returns the previous value (the PHP-observable contract); the size does not // currently change read granularity (reads return identical data). out.push_str(".comm _stream_chunk_size, 2048, 3\n"); - // _stream_connect_host: per-fd transport host string (ptr, len) captured by - // stream_socket_client so stream_socket_enable_crypto can default the TLS - // SNI / peer-name to the connection host when no ssl.peer_name context - // option is set. 256 fds * 16 bytes (ptr + len). A zero len means "unset". + // _stream_connect_host_fds/_stream_connect_host: bounded associative pairs + // mapping full-width descriptors to persisted transport host (ptr, len) + // values for stream_socket_enable_crypto SNI defaults. A zero len is empty. + out.push_str(".comm _stream_connect_host_fds, 2048, 3\n"); out.push_str(".comm _stream_connect_host, 4096, 3\n"); // _stream_notification_callback: the callable descriptor pointer for the // stream context's `notification` option, captured at codegen time by @@ -550,13 +652,6 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin // it at the CONNECT, COMPLETED, and FAILURE transfer milestones. Zero when // no notification callback is registered (the fire shim is then a no-op). out.push_str(".comm _stream_notification_callback, 8, 3\n"); - // _tls_peer_name_default: hardcoded peer-name buffer used as the SNI - // hint when stream_socket_enable_crypto is called without a context - // peer_name. v1 limitation — production TLS needs real peer-name - // passing via the stream context (deferred). - out.push_str( - ".globl _tls_peer_name_default\n_tls_peer_name_default:\n .ascii \"localhost\"\n", - ); // Key literals used by __rt_get_ssl_peer_name for the // _stream_context_options["ssl"]["peer_name"] lookup. out.push_str(".globl _ssl_key_str\n_ssl_key_str:\n .ascii \"ssl\"\n"); @@ -587,6 +682,9 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin out.push_str( ".globl _ssl_verify_peer_name_key_str\n_ssl_verify_peer_name_key_str:\n .ascii \"verify_peer_name\"\n", ); + out.push_str( + ".globl _ssl_crypto_method_key_str\n_ssl_crypto_method_key_str:\n .ascii \"crypto_method\"\n", + ); // Key literals + request fragments used by __rt_http_build_request. out.push_str(".globl _http_key_str\n_http_key_str:\n .ascii \"http\"\n"); out.push_str( @@ -874,7 +972,6 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin out.push_str(".globl _heap_dbg_clean_label\n_heap_dbg_clean_label:\n .ascii \"clean\\n\"\n"); out.push_str(".globl _heap_dbg_newline\n_heap_dbg_newline:\n .ascii \"\\n\"\n"); out.push_str(".globl _resource_id_prefix\n_resource_id_prefix:\n .ascii \"Resource id #\"\n"); - out.push_str(".globl _fmt_g\n_fmt_g:\n .asciz \"%.14G\"\n"); out.push_str(".globl _fmt_star_e\n_fmt_star_e:\n .asciz \"%.*e\"\n"); out.push_str(".globl _fmt_star_f\n_fmt_star_f:\n .asciz \"%.*f\"\n"); out.push_str(".globl _b64_encode_tbl\n_b64_encode_tbl:\n .ascii \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\"\n"); @@ -920,6 +1017,9 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin out.push_str(".globl _stat_key_blocks\n_stat_key_blocks:\n .ascii \"blocks\"\n"); out.push_str(".globl _dirname_dot\n_dirname_dot:\n .ascii \".\"\n"); out.push_str(".globl _dirname_slash\n_dirname_slash:\n .ascii \"/\"\n"); + // PHP_WIN32_IOUTIL_DEFAULT_SLASH: dirname() on Windows normalises a rooted + // path to a backslash regardless of which separator the input used. + out.push_str(".globl _dirname_backslash\n_dirname_backslash:\n .ascii \"\\\\\"\n"); out.push_str(&format!( ".globl _dirname_levels_msg\n_dirname_levels_msg:\n .ascii {:?}\n", DIRNAME_LEVELS_MSG @@ -944,7 +1044,18 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin out.push_str(".globl _meta_mode_w\n_meta_mode_w:\n .ascii \"w\"\n"); out.push_str(".globl _meta_mode_rw\n_meta_mode_rw:\n .ascii \"r+\"\n"); out.push_str(".p2align 3\n"); - out.push_str(".globl _tmpfile_template\n_tmpfile_template:\n .ascii \"/tmp/elephc-XXXXXX\\0\"\n .byte 0,0,0,0,0\n"); + // php builds its temporary path from php_get_temporary_directory rather than a + // fixed "/tmp": TMPDIR wins, and P_tmpdir is the fallback it returns verbatim + // ("/var/tmp/" on macOS, "/tmp" on Linux). __rt_php_temp_dir reads both of these. + out.push_str(".globl _tmpdir_env_name\n_tmpdir_env_name:\n .asciz \"TMPDIR\"\n"); + out.push_str(&format!( + ".globl _php_p_tmpdir\n_php_p_tmpdir:\n .asciz {:?}\n", + match target.platform { + Platform::MacOS => "/var/tmp/", + _ => "/tmp", + } + )); + out.push_str(".globl _tmpfile_suffix\n_tmpfile_suffix:\n .asciz \"/elephc-XXXXXX\"\n"); out.push_str(".globl _locale_utf8_name\n_locale_utf8_name:\n .asciz \"C.UTF-8\"\n"); out.push_str(".globl _locale_env_name\n_locale_env_name:\n .asciz \"\"\n"); out.push_str(&system::emit_json_data()); @@ -962,9 +1073,10 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin /// holding the total count, and `_callable_builtin_table` containing /// pointer/length pairs for each builtin. Used by the `is_callable()` runtime /// routine and callable-invoke paths. -fn emit_builtin_callable_data() -> String { +fn emit_builtin_callable_data(platform: Platform) -> String { let mut out = String::new(); - let builtins = supported_builtin_function_names(); + let builtins = + crate::types::checker::builtins::supported_builtin_function_names_on_platform(platform); for (idx, name) in builtins.iter().enumerate() { out.push_str(&format!( ".globl _callable_builtin_name_{0}\n_callable_builtin_name_{0}:\n .ascii \"{1}\"\n", @@ -985,6 +1097,24 @@ fn emit_builtin_callable_data() -> String { out } +#[cfg(test)] +mod tests { + use super::*; + + /// Verifies the runtime callable-name table follows PHP's target-specific + /// registration of link ownership functions without removing them on Unix. + #[test] + fn builtin_callable_data_filters_windows_lchown_functions() { + let windows = emit_builtin_callable_data(Platform::Windows); + assert!(!windows.contains(".ascii \"lchown\"")); + assert!(!windows.contains(".ascii \"lchgrp\"")); + + let linux = emit_builtin_callable_data(Platform::Linux); + assert!(linux.contains(".ascii \"lchown\"")); + assert!(linux.contains(".ascii \"lchgrp\"")); + } +} + /// Emit the `php_uname_mode_len_msg` and `php_uname_mode_value_msg` /// error message strings used when `php_uname()` mode argument validation fails. fn emit_php_uname_data() -> String { diff --git a/src/codegen_support/runtime/data/mod.rs b/src/codegen_support/runtime/data/mod.rs index 7a20f7bd7b..85ac96b88a 100644 --- a/src/codegen_support/runtime/data/mod.rs +++ b/src/codegen_support/runtime/data/mod.rs @@ -23,6 +23,9 @@ pub(crate) const PHP_UNAME_MODE_LEN_MSG: &str = /// Fatal error message when `php_uname()` receives a `$mode` argument that is not one of the supported single-character values. pub(crate) const PHP_UNAME_MODE_VALUE_MSG: &str = "Fatal error: php_uname(): Argument #1 ($mode) must be one of \"a\", \"m\", \"n\", \"r\", \"s\", or \"v\"\n"; +/// Notice emitted when `tempnam()` falls back from the requested directory. +pub(crate) const TEMPNAM_FALLBACK_NOTICE: &str = + "Notice: tempnam(): file created in the system's temporary directory\n"; /// Fatal error message when `dirname()` receives a `$levels` argument less than 1. /// ob_* PHP-parity diagnostics shared by the fixed data section and the /// output-buffering runtime emitters (which need the exact byte lengths). @@ -79,6 +82,12 @@ pub(crate) const DIRNAME_LEVELS_MSG: &str = /// Fatal error message when `str_repeat()` receives a `$times` argument less than 0. pub(crate) const STR_REPEAT_TIMES_MSG: &str = "Fatal error: str_repeat(): Argument #2 ($times) must be greater than or equal to 0\n"; +/// Fatal error message when `random_bytes()` receives a `$length` argument below 1. +pub(crate) const RANDOM_BYTES_LENGTH_MSG: &str = + "Fatal error: random_bytes(): Argument #1 ($length) must be greater than 0\n"; +/// Fatal error message when `random_bytes()` cannot obtain cryptographically secure random data. +pub(crate) const RANDOM_BYTES_SOURCE_MSG: &str = + "Fatal error: random_bytes(): Cannot gather sufficient random data\n"; /// Catchable `\ValueError` message when `hash()` receives an unknown algorithm name. pub(crate) const HASH_UNKNOWN_ALGO_MSG: &str = "hash(): Argument #1 ($algo) must be a valid hashing algorithm"; @@ -92,3 +101,21 @@ pub(crate) const HASH_HMAC_UNKNOWN_ALGO_MSG: &str = /// Catchable `\ValueError` message when `mb_strlen()` receives an unknown encoding name. pub(crate) const MB_STRLEN_UNKNOWN_ENCODING_MSG: &str = "mb_strlen(): Argument #2 ($encoding) must be a valid encoding"; +/// Catchable `\ValueError` message when `escapeshellarg()` receives a NUL byte. +pub(crate) const ESCAPE_SHELL_ARG_NUL_MSG: &str = + "escapeshellarg(): Argument #1 ($arg) must not contain any null bytes"; +/// Catchable `\ValueError` message when `escapeshellcmd()` receives a NUL byte. +pub(crate) const ESCAPE_SHELL_CMD_NUL_MSG: &str = + "escapeshellcmd(): Argument #1 ($command) must not contain any null bytes"; +/// Catchable Windows `\ValueError` when `escapeshellarg()` input exceeds php-src's cmd limit. +pub(crate) const ESCAPE_SHELL_ARG_INPUT_LENGTH_MSG: &str = + "Argument exceeds the allowed length of 8192 bytes"; +/// Catchable Windows `\ValueError` when `escapeshellarg()` output exceeds php-src's cmd limit. +pub(crate) const ESCAPE_SHELL_ARG_OUTPUT_LENGTH_MSG: &str = + "Escaped argument exceeds the allowed length of 8192 bytes"; +/// Catchable Windows `\ValueError` when `escapeshellcmd()` input exceeds php-src's cmd limit. +pub(crate) const ESCAPE_SHELL_CMD_INPUT_LENGTH_MSG: &str = + "Command exceeds the allowed length of 8192 bytes"; +/// Catchable Windows `\ValueError` when `escapeshellcmd()` output exceeds php-src's cmd limit. +pub(crate) const ESCAPE_SHELL_CMD_OUTPUT_LENGTH_MSG: &str = + "Escaped command exceeds the allowed length of 8192 bytes"; diff --git a/src/codegen_support/runtime/emitters.rs b/src/codegen_support/runtime/emitters.rs index 6e0e3ba0d2..5cea07cc3f 100644 --- a/src/codegen_support/runtime/emitters.rs +++ b/src/codegen_support/runtime/emitters.rs @@ -23,8 +23,10 @@ use super::pointers; use super::spl; use super::strings; use super::system; +use super::win32; use super::zval; use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Platform; use crate::codegen_support::RuntimeFeatures; /// Emits all runtime helper labels in dependency order for supported targets. @@ -35,6 +37,15 @@ use crate::codegen_support::RuntimeFeatures; /// Each category is emitted before any code that depends on it, ensuring labels /// are available when branches are assembled. pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) { + if features.tls { + crate::codegen_support::tls::emit_tls_abi_adapters(emitter); + } + if emitter.platform == Platform::Windows { + win32::emit_win32_shims(emitter, features); + win32::emit_fd_to_handle(emitter); + win32::emit_main_wrapper(emitter); + } + diagnostics::emit_diagnostics(emitter); // String runtime functions @@ -42,6 +53,7 @@ pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) { strings::emit_resource_to_string(emitter); strings::emit_resource_write_stdout(emitter); strings::emit_ftoa(emitter); + strings::emit_var_dump_ftoa(emitter); strings::emit_concat(emitter); strings::emit_atoi(emitter); strings::emit_str_eq(emitter); @@ -49,6 +61,7 @@ pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) { strings::emit_str_looks_like_int_for_coercion(emitter); strings::emit_str_to_int(emitter); strings::emit_str_loose_eq(emitter); + strings::emit_php_round(emitter); strings::emit_number_format(emitter); strings::emit_strcopy(emitter); strings::emit_str_persist(emitter); @@ -91,6 +104,7 @@ pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) { strings::emit_urlencode(emitter); strings::emit_urldecode(emitter); strings::emit_rawurlencode(emitter); + strings::emit_shell_escapes(emitter); strings::emit_md5(emitter); strings::emit_sha1(emitter); strings::emit_crc32(emitter); @@ -173,8 +187,16 @@ pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) { exceptions::emit_class_implements_interface(emitter); exceptions::emit_dynamic_instanceof(emitter); exceptions::emit_exception_matches(emitter); + exceptions::emit_throw_static_exception(emitter); exceptions::emit_throw_current(emitter); exceptions::emit_rethrow_current(emitter); + // Windows x86_64 only: emit elephc's SEH-free setjmp/longjmp. The MinGW C-library + // versions are SEH-based (they walk the stack and unwind via RtlUnwindEx) and read + // their arguments MSx64-style, both of which break every try/catch and Fiber on + // Windows; `bl_c` routes `setjmp`/`longjmp` to these labels on that target. + if emitter.platform == Platform::Windows { + exceptions::emit_setjmp_longjmp(emitter); + } // Generator runtime helpers for Iterator methods, send/throw, and return-value retrieval. generators::emit_generator_runtime(emitter); @@ -205,7 +227,9 @@ pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) { arrays::emit_array_hash_union(emitter); arrays::emit_hash_array_union(emitter); arrays::emit_random_u32(emitter); + arrays::emit_random_bytes(emitter); arrays::emit_random_uniform(emitter); + arrays::emit_random_uniform64(emitter); arrays::emit_sort_int(emitter, false); arrays::emit_sort_int(emitter, true); arrays::emit_sort_str(emitter, false); @@ -377,6 +401,7 @@ pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) { io::emit_http_response_code(emitter, features.web); io::emit_header(emitter, features.web); io::emit_cstr(emitter); + io::emit_php_temp_dir(emitter); io::emit_disk_space(emitter); io::emit_fopen(emitter); io::emit_fgets(emitter); @@ -419,6 +444,11 @@ pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) { io::emit_stream_socket_pair(emitter); io::emit_popen(emitter); io::emit_pclose(emitter); + io::emit_proc_open(emitter); + io::emit_proc_open_marshalling(emitter); + io::emit_proc_pipe_registry(emitter); + io::emit_proc_status(emitter); + io::emit_proc_close(emitter); io::emit_opendir(emitter); io::emit_readdir(emitter); io::emit_closedir(emitter); @@ -437,6 +467,7 @@ pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) { io::emit_stream_context_set_option_4(emitter); io::emit_get_string_context_option(emitter); io::emit_get_int_context_option(emitter); + io::emit_get_bool_context_option(emitter); io::emit_apply_socket_client_opts(emitter); io::emit_apply_socket_server_opts(emitter); io::emit_socket_backlog(emitter); @@ -445,6 +476,7 @@ pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) { io::emit_http_build_request(emitter); io::emit_fread(emitter); io::emit_fwrite(emitter); + io::emit_tls_session_table(emitter); io::emit_user_wrapper_fclose(emitter); io::emit_user_wrapper_fread(emitter); io::emit_user_wrapper_fwrite(emitter); @@ -469,6 +501,7 @@ pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) { io::emit_user_wrapper_dir_rewinddir(emitter); io::emit_touch_meta_array(emitter); io::emit_stash_connect_host(emitter); + io::emit_addr_tls_crypto_method(emitter); io::emit_fire_notification(emitter); io::emit_user_wrapper_stream_cast(emitter); io::emit_stream_filter_register(emitter); @@ -651,6 +684,28 @@ mod tests { assert!(included.output().contains("__rt_mb_strlen:")); } + /// Verifies Windows routes mb_strlen's libiconv calls through the SysV-to-MS x64 shims. + #[test] + fn test_windows_mb_strlen_uses_iconv_abi_shims() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_runtime( + &mut emitter, + RuntimeFeatures { + mb_strlen: true, + iconv: true, + ..RuntimeFeatures::none() + }, + ); + let asm = emitter.output(); + + assert!(asm.contains("call __rt_sys_iconv_open")); + assert!(asm.contains("call __rt_sys_iconv\n")); + assert!(asm.contains("call __rt_sys_iconv_close")); + assert!(!asm.contains(" call iconv_open\n")); + assert!(!asm.contains(" call iconv\n")); + assert!(!asm.contains(" call iconv_close\n")); + } + /// Verifies that Linux x86_64 uses the shared runtime surface. #[test] fn test_linux_x86_64_runtime_uses_shared_surface() { @@ -675,6 +730,51 @@ mod tests { } } + /// Regression net for wiring the Windows syscall→shim transform into the + /// runtime build (FIX #1). Generates the full shared x86_64 runtime for a + /// Windows target and applies `transform_for_windows` exactly as the runtime + /// cache does, then asserts that no raw `syscall` instruction and no `int3` + /// (unmapped-syscall marker) survive. `int3` would only appear if the runtime + /// emits a syscall number missing from `linux_syscall_to_shim`, so this also + /// guards syscall→shim coverage. Needs no MinGW toolchain. + #[test] + fn test_windows_runtime_has_no_raw_syscalls_after_transform() { + let target = Target::new(Platform::Windows, Arch::X86_64); + let raw = crate::codegen::generate_runtime_with_features_pic( + 8 * 1024 * 1024, + target, + RuntimeFeatures::all(), + false, + ); + + // Sanity: the untransformed shared x86_64 runtime really does emit raw + // syscalls, so a passing assertion below is not vacuous. + assert!( + raw.lines().any(|line| line.trim() == "syscall"), + "expected the raw x86_64 runtime to contain standalone syscall instructions" + ); + + let transformed = crate::codegen::platform::transform_for_windows(&raw); + + let leftover: Vec<&str> = transformed + .lines() + .filter(|line| { + let t = line.trim(); + t == "syscall" || t.starts_with("syscall ") || t.starts_with("syscall\t") + }) + .collect(); + assert!( + leftover.is_empty(), + "Windows runtime still contains raw syscall lines after transform: {:?}", + leftover + ); + assert!( + !transformed.contains("int3"), + "Windows runtime transform produced int3 — an unmapped Linux syscall number \ + is emitted by the runtime but missing from linux_syscall_to_shim" + ); + } + /// Verifies the full macOS AArch64 runtime still assembles once per-symbol /// dead stripping is enabled. The real codegen path renames internal labels /// to `L`-locals and appends a `.subsections_via_symbols` footer; under that @@ -744,11 +844,13 @@ mod tests { // A token is an internal helper label iff it is an `L`-localized `__rt_*` // name (what `label()` produces under dead stripping). `.alt_entry` // helpers stay bare `__rt_*`, so they never match here. + /// Reports whether a token names a localized internal runtime helper. fn is_internal(tok: &str) -> bool { tok.starts_with("L__rt_") } // True when `s` is a bare label definition body (no whitespace, label // characters only, not purely numeric → not an assembler-local `N:`). + /// Reports whether text is a valid non-numeric assembler label body. fn is_label_name(s: &str) -> bool { !s.is_empty() && !s.bytes().all(|b| b.is_ascii_digit()) diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 4c56867726..42e81991f9 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -1216,7 +1216,7 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("bl __rt_mixed_cast_float"); // cast the right boxed operand to a PHP numeric double emitter.instruction("fmov d1, d0"); // move the exponent into libc pow's second argument emitter.instruction("ldr d0, [sp, #8]"); // reload the base into libc pow's first argument - emitter.bl_c("pow"); + emitter.emit_call_c("pow"); emitter.instruction("fmov x1, d0"); // move the pow result bits into mixed value_lo emitter.instruction("mov x2, xzr"); // double payloads do not use a high word emitter.instruction("mov x0, #2"); // runtime tag 2 = double @@ -1234,7 +1234,7 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("bl __rt_mixed_cast_float"); // cast the boxed eval value to a PHP numeric double emitter.instruction("ldr x2, [sp, #8]"); // reload the precision-presence flag after the value cast emitter.instruction("cbnz x2, __elephc_eval_value_round_precision"); // use the precision path when a second argument is present - emitter.bl_c("round"); + emitter.emit_call_c("round"); emitter.instruction("b __elephc_eval_value_round_box"); // box the default-precision round result emitter.label("__elephc_eval_value_round_precision"); emitter.instruction("str d0, [sp, #16]"); // save the original value while casting the precision @@ -1242,12 +1242,12 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("bl __rt_mixed_cast_int"); // cast the optional precision to a PHP integer emitter.instruction("scvtf d1, x0"); // convert the precision to a floating exponent for pow emitter.instruction("fmov d0, #10.0"); // materialize 10.0 as the precision multiplier base - emitter.bl_c("pow"); + emitter.emit_call_c("pow"); emitter.instruction("ldr d1, [sp, #16]"); // reload the original value after pow returns the multiplier emitter.instruction("fmul d1, d1, d0"); // scale the value by the precision multiplier emitter.instruction("str d0, [sp, #24]"); // save the multiplier for rescaling after round emitter.instruction("fmov d0, d1"); // move the scaled value into the round argument - emitter.bl_c("round"); + emitter.emit_call_c("round"); emitter.instruction("ldr d1, [sp, #24]"); // reload the precision multiplier for rescaling emitter.instruction("fdiv d0, d0, d1"); // scale the rounded value back to requested precision emitter.label("__elephc_eval_value_round_box"); @@ -2901,7 +2901,7 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("call __rt_mixed_cast_float"); // cast the right boxed operand to a PHP numeric double emitter.instruction("movapd xmm1, xmm0"); // move the right divisor into the second fmod argument emitter.instruction("movsd xmm0, QWORD PTR [rbp - 16]"); // move the left dividend into the first fmod argument - emitter.bl_c("fmod"); + emitter.emit_call_c("fmod"); // route the SysV-staged operands through the MSx64 shim on Windows emitter.instruction("ucomisd xmm0, xmm0"); // detect NaN so PHP echo prints NAN without a sign emitter.instruction("jp __elephc_eval_value_fmod_nan_x86"); // normalize unordered fmod results before boxing emitter.instruction("movq rdi, xmm0"); // move the fmod result bits into mixed value_lo @@ -3005,7 +3005,7 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("call __rt_mixed_cast_float"); // cast the right boxed operand to a PHP numeric double emitter.instruction("movapd xmm1, xmm0"); // move the exponent into libc pow's second argument emitter.instruction("movsd xmm0, QWORD PTR [rbp - 16]"); // reload the base into libc pow's first argument - emitter.bl_c("pow"); + emitter.emit_call_c("pow"); emitter.instruction("movq rdi, xmm0"); // move the pow result bits into mixed value_lo emitter.instruction("xor esi, esi"); // double payloads do not use a high word emitter.instruction("mov eax, 2"); // runtime tag 2 = double @@ -3024,7 +3024,7 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("call __rt_mixed_cast_float"); // cast the boxed eval value to a PHP numeric double emitter.instruction("cmp QWORD PTR [rbp - 16], 0"); // check whether a precision argument was supplied emitter.instruction("jne __elephc_eval_value_round_precision_x86"); // use the precision path when a second argument is present - emitter.bl_c("round"); + emitter.emit_call_c("round"); emitter.instruction("jmp __elephc_eval_value_round_box_x86"); // box the default-precision round result emitter.label("__elephc_eval_value_round_precision_x86"); emitter.instruction("movsd QWORD PTR [rbp - 24], xmm0"); // save the original value while casting the precision @@ -3033,12 +3033,12 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("cvtsi2sd xmm1, rax"); // convert the precision to a floating exponent for pow emitter.instruction("mov rax, 0x4024000000000000"); // materialize the IEEE-754 payload for 10.0 emitter.instruction("movq xmm0, rax"); // move 10.0 into the pow base argument - emitter.bl_c("pow"); + emitter.emit_call_c("pow"); emitter.instruction("movsd xmm1, QWORD PTR [rbp - 24]"); // reload the original value after pow returns the multiplier emitter.instruction("mulsd xmm1, xmm0"); // scale the value by the precision multiplier emitter.instruction("movsd QWORD PTR [rbp - 32], xmm0"); // save the multiplier for rescaling after round emitter.instruction("movsd xmm0, xmm1"); // move the scaled value into the round argument - emitter.bl_c("round"); + emitter.emit_call_c("round"); emitter.instruction("movsd xmm1, QWORD PTR [rbp - 32]"); // reload the precision multiplier for rescaling emitter.instruction("divsd xmm0, xmm1"); // scale the rounded value back to requested precision emitter.label("__elephc_eval_value_round_box_x86"); @@ -3805,7 +3805,8 @@ fn emit_aarch64_eval_reflection_class_trait_alias_sources(emitter: &mut Emitter) /// Emits the ARM64 eval hook that returns the AOT reflection source file. fn emit_aarch64_eval_reflection_source_file(emitter: &mut Emitter) { - let string_symbol = emitter.target.extern_symbol("__elephc_eval_value_string"); + let string_symbol = + abi::c_callback_internal_symbol(emitter.target, "__elephc_eval_value_string"); label_c_global(emitter, "__elephc_eval_reflection_source_file"); abi::emit_symbol_address(emitter, "x9", "_eval_reflection_source_file_len"); emitter.instruction("ldr x1, [x9]"); // load the generated source-file length @@ -3831,12 +3832,14 @@ fn emit_aarch64_eval_reflection_member_names( let skip_label = format!("{label_prefix}_skip"); let miss_label = format!("{label_prefix}_miss"); let done_label = format!("{label_prefix}_done"); - let string_array_new_symbol = emitter - .target - .extern_symbol("__elephc_eval_value_string_array_new"); - let string_array_push_symbol = emitter - .target - .extern_symbol("__elephc_eval_value_string_array_push"); + let string_array_new_symbol = abi::c_callback_internal_symbol( + emitter.target, + "__elephc_eval_value_string_array_new", + ); + let string_array_push_symbol = abi::c_callback_internal_symbol( + emitter.target, + "__elephc_eval_value_string_array_push", + ); label_c_global(emitter, symbol); emitter.instruction("sub sp, sp, #112"); // reserve scan state across allocation and string comparisons emitter.instruction("stp x29, x30, [sp, #96]"); // save frame pointer and return address across helper calls @@ -4248,7 +4251,8 @@ fn emit_x86_64_eval_reflection_class_trait_alias_sources(emitter: &mut Emitter) /// Emits the x86_64 eval hook that returns the AOT reflection source file. fn emit_x86_64_eval_reflection_source_file(emitter: &mut Emitter) { - let string_symbol = emitter.target.extern_symbol("__elephc_eval_value_string"); + let string_symbol = + abi::c_callback_internal_symbol(emitter.target, "__elephc_eval_value_string"); label_c_global(emitter, "__elephc_eval_reflection_source_file"); abi::emit_symbol_address(emitter, "r10", "_eval_reflection_source_file_len"); emitter.instruction("mov rsi, QWORD PTR [r10]"); // load the generated source-file length @@ -4275,12 +4279,14 @@ fn emit_x86_64_eval_reflection_member_names( let skip_label = format!("{label_prefix}_skip"); let miss_label = format!("{label_prefix}_miss"); let done_label = format!("{label_prefix}_done"); - let string_array_new_symbol = emitter - .target - .extern_symbol("__elephc_eval_value_string_array_new"); - let string_array_push_symbol = emitter - .target - .extern_symbol("__elephc_eval_value_string_array_push"); + let string_array_new_symbol = abi::c_callback_internal_symbol( + emitter.target, + "__elephc_eval_value_string_array_new", + ); + let string_array_push_symbol = abi::c_callback_internal_symbol( + emitter.target, + "__elephc_eval_value_string_array_push", + ); label_c_global(emitter, symbol); emitter.instruction("push rbp"); // preserve the Rust caller frame pointer emitter.instruction("mov rbp, rsp"); // establish a stable member-name scan frame pointer @@ -5134,6 +5140,5 @@ fn emit_x86_64_reject_runtime_managed_clone_classes( /// Emits a global label with platform C-symbol mangling. fn label_c_global(emitter: &mut Emitter, name: &str) { - let symbol = emitter.target.extern_symbol(name); - emitter.label_global(&symbol); + abi::emit_c_callback_entry(emitter, name); } diff --git a/src/codegen_support/runtime/eval_scope.rs b/src/codegen_support/runtime/eval_scope.rs index dfe2c6f8bb..f13765248b 100644 --- a/src/codegen_support/runtime/eval_scope.rs +++ b/src/codegen_support/runtime/eval_scope.rs @@ -12,6 +12,7 @@ //! name pointers instead of copying name bytes. //! - Owned Mixed cells are released on overwrite and scope free. +use crate::codegen::abi; use crate::codegen::emit::Emitter; use crate::codegen::platform::Arch; @@ -43,182 +44,182 @@ fn emit_aarch64_eval_scope_runtime(emitter: &mut Emitter) { /// Emits the ARM64 scope allocator. fn emit_aarch64_eval_scope_new(emitter: &mut Emitter) { label_c_global(emitter, "__elephc_eval_scope_new"); - emitter.instruction("stp x29, x30, [sp, #-16]!"); // preserve the caller frame while allocating the scope header - emitter.instruction("mov x29, sp"); // establish a stable frame for the nested allocator call - emitter.instruction("mov x0, #8"); // scope header stores one head pointer - emitter.instruction("bl __rt_heap_alloc"); // allocate the scope header from the core heap - emitter.instruction("str xzr, [x0]"); // initialize the entry-list head to null - emitter.instruction("ldp x29, x30, [sp], #16"); // restore the caller frame after allocation - emitter.instruction("ret"); // return the scope handle in x0 + emitter.instruction("stp x29, x30, [sp, #-16]!"); // preserve the caller frame while allocating the scope header + emitter.instruction("mov x29, sp"); // establish a stable frame for the nested allocator call + emitter.instruction("mov x0, #8"); // scope header stores one head pointer + emitter.instruction("bl __rt_heap_alloc"); // allocate the scope header from the core heap + emitter.instruction("str xzr, [x0]"); // initialize the entry-list head to null + emitter.instruction("ldp x29, x30, [sp], #16"); // restore the caller frame after allocation + emitter.instruction("ret"); // return the scope handle in x0 } /// Emits the ARM64 scope destructor. fn emit_aarch64_eval_scope_free(emitter: &mut Emitter) { label_c_global(emitter, "__elephc_eval_scope_free"); - emitter.instruction("cbz x0, __elephc_eval_scope_free_done"); // null scope handles are already free - emitter.instruction("sub sp, sp, #48"); // reserve a frame for callee-saved scan state - emitter.instruction("stp x29, x30, [sp, #0]"); // save frame pointer and return address across release calls - emitter.instruction("stp x19, x20, [sp, #16]"); // preserve scope and current-entry registers - emitter.instruction("str x21, [sp, #32]"); // preserve the next-entry register - emitter.instruction("mov x29, sp"); // establish a stable frame for nested runtime calls - emitter.instruction("mov x19, x0"); // keep the scope header pointer across entry releases - emitter.instruction("ldr x20, [x19]"); // load the first entry in the scope list + emitter.instruction("cbz x0, __elephc_eval_scope_free_done"); // null scope handles are already free + emitter.instruction("sub sp, sp, #48"); // reserve a frame for callee-saved scan state + emitter.instruction("stp x29, x30, [sp, #0]"); // save frame pointer and return address across release calls + emitter.instruction("stp x19, x20, [sp, #16]"); // preserve scope and current-entry registers + emitter.instruction("str x21, [sp, #32]"); // preserve the next-entry register + emitter.instruction("mov x29, sp"); // establish a stable frame for nested runtime calls + emitter.instruction("mov x19, x0"); // keep the scope header pointer across entry releases + emitter.instruction("ldr x20, [x19]"); // load the first entry in the scope list emitter.label("__elephc_eval_scope_free_loop"); - emitter.instruction("cbz x20, __elephc_eval_scope_free_header"); // stop when every entry has been released - emitter.instruction("ldr x21, [x20]"); // save entry->next before freeing the current entry - emitter.instruction("ldr x9, [x20, #32]"); // load the entry ABI flags - emitter.instruction(&format!("tst x9, #{}", EVAL_SCOPE_FLAG_OWNED)); // check whether the scope owns this Mixed cell - emitter.instruction("b.eq __elephc_eval_scope_free_entry"); // borrowed cells are not released by the scope - emitter.instruction("ldr x0, [x20, #24]"); // load the owned Mixed cell pointer - emitter.instruction("cbz x0, __elephc_eval_scope_free_entry"); // tolerate null owned cells defensively - emitter.instruction("bl __rt_decref_mixed"); // release the scope-owned Mixed cell + emitter.instruction("cbz x20, __elephc_eval_scope_free_header"); // stop when every entry has been released + emitter.instruction("ldr x21, [x20]"); // save entry->next before freeing the current entry + emitter.instruction("ldr x9, [x20, #32]"); // load the entry ABI flags + emitter.instruction(&format!("tst x9, #{}", EVAL_SCOPE_FLAG_OWNED)); // check whether the scope owns this Mixed cell + emitter.instruction("b.eq __elephc_eval_scope_free_entry"); // borrowed cells are not released by the scope + emitter.instruction("ldr x0, [x20, #24]"); // load the owned Mixed cell pointer + emitter.instruction("cbz x0, __elephc_eval_scope_free_entry"); // tolerate null owned cells defensively + emitter.instruction("bl __rt_decref_mixed"); // release the scope-owned Mixed cell emitter.label("__elephc_eval_scope_free_entry"); - emitter.instruction("mov x0, x20"); // pass the current entry allocation to the heap free helper - emitter.instruction("bl __rt_heap_free"); // release the entry record itself - emitter.instruction("mov x20, x21"); // advance to the saved next entry - emitter.instruction("b __elephc_eval_scope_free_loop"); // continue freeing entries + emitter.instruction("mov x0, x20"); // pass the current entry allocation to the heap free helper + emitter.instruction("bl __rt_heap_free"); // release the entry record itself + emitter.instruction("mov x20, x21"); // advance to the saved next entry + emitter.instruction("b __elephc_eval_scope_free_loop"); // continue freeing entries emitter.label("__elephc_eval_scope_free_header"); - emitter.instruction("mov x0, x19"); // pass the scope header allocation to the heap free helper - emitter.instruction("bl __rt_heap_free"); // release the scope header after all entries - emitter.instruction("ldr x21, [sp, #32]"); // restore the saved next-entry register - emitter.instruction("ldp x19, x20, [sp, #16]"); // restore callee-saved scan registers - emitter.instruction("ldp x29, x30, [sp, #0]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #48"); // release the scope-free frame + emitter.instruction("mov x0, x19"); // pass the scope header allocation to the heap free helper + emitter.instruction("bl __rt_heap_free"); // release the scope header after all entries + emitter.instruction("ldr x21, [sp, #32]"); // restore the saved next-entry register + emitter.instruction("ldp x19, x20, [sp, #16]"); // restore callee-saved scan registers + emitter.instruction("ldp x29, x30, [sp, #0]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the scope-free frame emitter.label("__elephc_eval_scope_free_done"); - emitter.instruction("ret"); // return after the scope is fully released + emitter.instruction("ret"); // return after the scope is fully released } /// Emits the ARM64 scope setter. fn emit_aarch64_eval_scope_set(emitter: &mut Emitter) { label_c_global(emitter, "__elephc_eval_scope_set"); - emitter.instruction("cbz x0, __elephc_eval_scope_set_fatal"); // reject null scope handles - emitter.instruction("cbz x2, __elephc_eval_scope_set_frame"); // empty names do not need a readable pointer - emitter.instruction("cbz x1, __elephc_eval_scope_set_fatal"); // non-empty names must provide bytes + emitter.instruction("cbz x0, __elephc_eval_scope_set_fatal"); // reject null scope handles + emitter.instruction("cbz x2, __elephc_eval_scope_set_frame"); // empty names do not need a readable pointer + emitter.instruction("cbz x1, __elephc_eval_scope_set_fatal"); // non-empty names must provide bytes emitter.label("__elephc_eval_scope_set_frame"); - emitter.instruction("sub sp, sp, #96"); // reserve a frame for inputs and callee-saved scan state - emitter.instruction("stp x29, x30, [sp, #0]"); // save frame pointer and return address across runtime calls - emitter.instruction("stp x19, x20, [sp, #16]"); // preserve scope and name pointer - emitter.instruction("stp x21, x22, [sp, #32]"); // preserve name length and cell pointer - emitter.instruction("stp x23, x24, [sp, #48]"); // preserve flags and previous-next pointer - emitter.instruction("stp x25, x26, [sp, #64]"); // preserve current entry and allocated entry pointer - emitter.instruction("mov x29, sp"); // establish a stable frame for nested runtime calls - emitter.instruction("mov x19, x0"); // save scope header pointer - emitter.instruction("mov x20, x1"); // save name byte pointer - emitter.instruction("mov x21, x2"); // save name byte length - emitter.instruction("mov x22, x3"); // save Mixed cell pointer - emitter.instruction("mov x23, x4"); // save caller-provided ABI flags - emitter.instruction("mov x24, x19"); // previous-next pointer initially addresses scope->head - emitter.instruction("ldr x25, [x24]"); // load the first candidate entry + emitter.instruction("sub sp, sp, #96"); // reserve a frame for inputs and callee-saved scan state + emitter.instruction("stp x29, x30, [sp, #0]"); // save frame pointer and return address across runtime calls + emitter.instruction("stp x19, x20, [sp, #16]"); // preserve scope and name pointer + emitter.instruction("stp x21, x22, [sp, #32]"); // preserve name length and cell pointer + emitter.instruction("stp x23, x24, [sp, #48]"); // preserve flags and previous-next pointer + emitter.instruction("stp x25, x26, [sp, #64]"); // preserve current entry and allocated entry pointer + emitter.instruction("mov x29, sp"); // establish a stable frame for nested runtime calls + emitter.instruction("mov x19, x0"); // save scope header pointer + emitter.instruction("mov x20, x1"); // save name byte pointer + emitter.instruction("mov x21, x2"); // save name byte length + emitter.instruction("mov x22, x3"); // save Mixed cell pointer + emitter.instruction("mov x23, x4"); // save caller-provided ABI flags + emitter.instruction("mov x24, x19"); // previous-next pointer initially addresses scope->head + emitter.instruction("ldr x25, [x24]"); // load the first candidate entry emitter.label("__elephc_eval_scope_set_probe"); - emitter.instruction("cbz x25, __elephc_eval_scope_set_insert"); // insert when the name is absent - emitter.instruction("ldr x9, [x25, #16]"); // load candidate name length - emitter.instruction("cmp x9, x21"); // compare candidate length with requested length - emitter.instruction("b.ne __elephc_eval_scope_set_next"); // different lengths cannot match - emitter.instruction("ldr x10, [x25, #8]"); // load candidate name bytes - emitter.instruction("mov x11, #0"); // byte index for the equality loop + emitter.instruction("cbz x25, __elephc_eval_scope_set_insert"); // insert when the name is absent + emitter.instruction("ldr x9, [x25, #16]"); // load candidate name length + emitter.instruction("cmp x9, x21"); // compare candidate length with requested length + emitter.instruction("b.ne __elephc_eval_scope_set_next"); // different lengths cannot match + emitter.instruction("ldr x10, [x25, #8]"); // load candidate name bytes + emitter.instruction("mov x11, #0"); // byte index for the equality loop emitter.label("__elephc_eval_scope_set_cmp"); - emitter.instruction("cmp x11, x21"); // have all bytes matched? - emitter.instruction("b.eq __elephc_eval_scope_set_update"); // equal length and bytes select this entry - emitter.instruction("ldrb w12, [x10, x11]"); // load one existing name byte - emitter.instruction("ldrb w13, [x20, x11]"); // load the corresponding requested name byte - emitter.instruction("cmp w12, w13"); // compare candidate and requested name bytes - emitter.instruction("b.ne __elephc_eval_scope_set_next"); // any byte mismatch means this entry is not the target - emitter.instruction("add x11, x11, #1"); // advance to the next byte - emitter.instruction("b __elephc_eval_scope_set_cmp"); // continue comparing this candidate name + emitter.instruction("cmp x11, x21"); // have all bytes matched? + emitter.instruction("b.eq __elephc_eval_scope_set_update"); // equal length and bytes select this entry + emitter.instruction("ldrb w12, [x10, x11]"); // load one existing name byte + emitter.instruction("ldrb w13, [x20, x11]"); // load the corresponding requested name byte + emitter.instruction("cmp w12, w13"); // compare candidate and requested name bytes + emitter.instruction("b.ne __elephc_eval_scope_set_next"); // any byte mismatch means this entry is not the target + emitter.instruction("add x11, x11, #1"); // advance to the next byte + emitter.instruction("b __elephc_eval_scope_set_cmp"); // continue comparing this candidate name emitter.label("__elephc_eval_scope_set_next"); - emitter.instruction("mov x24, x25"); // previous-next pointer now addresses current->next - emitter.instruction("ldr x25, [x25]"); // advance to the next entry - emitter.instruction("b __elephc_eval_scope_set_probe"); // continue scanning the entry list + emitter.instruction("mov x24, x25"); // previous-next pointer now addresses current->next + emitter.instruction("ldr x25, [x25]"); // advance to the next entry + emitter.instruction("b __elephc_eval_scope_set_probe"); // continue scanning the entry list emitter.label("__elephc_eval_scope_set_update"); - emitter.instruction("ldr x9, [x25, #32]"); // load old entry flags - emitter.instruction(&format!("tst x9, #{}", EVAL_SCOPE_FLAG_OWNED)); // check whether the old cell is scope-owned - emitter.instruction("b.eq __elephc_eval_scope_set_store"); // borrowed old cells do not need release - emitter.instruction("ldr x0, [x25, #24]"); // load the old Mixed cell pointer - emitter.instruction("cmp x0, x22"); // compare old and replacement cells - emitter.instruction("b.eq __elephc_eval_scope_set_store"); // retaining the same cell must not decref it - emitter.instruction("cbz x0, __elephc_eval_scope_set_store"); // tolerate null old cells defensively - emitter.instruction("bl __rt_decref_mixed"); // release the overwritten owned Mixed cell + emitter.instruction("ldr x9, [x25, #32]"); // load old entry flags + emitter.instruction(&format!("tst x9, #{}", EVAL_SCOPE_FLAG_OWNED)); // check whether the old cell is scope-owned + emitter.instruction("b.eq __elephc_eval_scope_set_store"); // borrowed old cells do not need release + emitter.instruction("ldr x0, [x25, #24]"); // load the old Mixed cell pointer + emitter.instruction("cmp x0, x22"); // compare old and replacement cells + emitter.instruction("b.eq __elephc_eval_scope_set_store"); // retaining the same cell must not decref it + emitter.instruction("cbz x0, __elephc_eval_scope_set_store"); // tolerate null old cells defensively + emitter.instruction("bl __rt_decref_mixed"); // release the overwritten owned Mixed cell emitter.label("__elephc_eval_scope_set_store"); - emitter.instruction("str x22, [x25, #24]"); // store the replacement Mixed cell + emitter.instruction("str x22, [x25, #24]"); // store the replacement Mixed cell emitter.instruction(&format!("mov x9, #{}", EVAL_SCOPE_FLAG_PRESENT | EVAL_SCOPE_FLAG_DIRTY)); // materialize visible and dirty ABI bits - emitter.instruction("orr x9, x23, x9"); // merge caller ownership flags with core visibility flags - emitter.instruction("str x9, [x25, #32]"); // publish the updated ABI flags - emitter.instruction(&format!("mov x0, #{}", EVAL_STATUS_OK)); // report successful scope write - emitter.instruction("b __elephc_eval_scope_set_done"); // restore the frame and return + emitter.instruction("orr x9, x23, x9"); // merge caller ownership flags with core visibility flags + emitter.instruction("str x9, [x25, #32]"); // publish the updated ABI flags + emitter.instruction(&format!("mov x0, #{}", EVAL_STATUS_OK)); // report successful scope write + emitter.instruction("b __elephc_eval_scope_set_done"); // restore the frame and return emitter.label("__elephc_eval_scope_set_insert"); - emitter.instruction("mov x0, #40"); // entry records store next, name, length, cell, and flags - emitter.instruction("bl __rt_heap_alloc"); // allocate a new entry record - emitter.instruction("mov x26, x0"); // keep the new entry pointer while initializing fields - emitter.instruction("str x25, [x26]"); // new entry next points at the old successor, normally null - emitter.instruction("str x20, [x26, #8]"); // store the borrowed static name pointer - emitter.instruction("str x21, [x26, #16]"); // store the name byte length - emitter.instruction("str x22, [x26, #24]"); // store the Mixed cell pointer + emitter.instruction("mov x0, #40"); // entry records store next, name, length, cell, and flags + emitter.instruction("bl __rt_heap_alloc"); // allocate a new entry record + emitter.instruction("mov x26, x0"); // keep the new entry pointer while initializing fields + emitter.instruction("str x25, [x26]"); // new entry next points at the old successor, normally null + emitter.instruction("str x20, [x26, #8]"); // store the borrowed static name pointer + emitter.instruction("str x21, [x26, #16]"); // store the name byte length + emitter.instruction("str x22, [x26, #24]"); // store the Mixed cell pointer emitter.instruction(&format!("mov x9, #{}", EVAL_SCOPE_FLAG_PRESENT | EVAL_SCOPE_FLAG_DIRTY)); // materialize visible and dirty ABI bits - emitter.instruction("orr x9, x23, x9"); // merge caller ownership flags with core visibility flags - emitter.instruction("str x9, [x26, #32]"); // store the new entry flags - emitter.instruction("str x26, [x24]"); // link the new entry through the previous-next pointer - emitter.instruction(&format!("mov x0, #{}", EVAL_STATUS_OK)); // report successful scope write + emitter.instruction("orr x9, x23, x9"); // merge caller ownership flags with core visibility flags + emitter.instruction("str x9, [x26, #32]"); // store the new entry flags + emitter.instruction("str x26, [x24]"); // link the new entry through the previous-next pointer + emitter.instruction(&format!("mov x0, #{}", EVAL_STATUS_OK)); // report successful scope write emitter.label("__elephc_eval_scope_set_done"); - emitter.instruction("ldp x25, x26, [sp, #64]"); // restore callee-saved entry registers - emitter.instruction("ldp x23, x24, [sp, #48]"); // restore callee-saved flag and link registers - emitter.instruction("ldp x21, x22, [sp, #32]"); // restore callee-saved name length and cell registers - emitter.instruction("ldp x19, x20, [sp, #16]"); // restore callee-saved scope and name registers - emitter.instruction("ldp x29, x30, [sp, #0]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #96"); // release the setter frame - emitter.instruction("ret"); // return the eval status in x0 + emitter.instruction("ldp x25, x26, [sp, #64]"); // restore callee-saved entry registers + emitter.instruction("ldp x23, x24, [sp, #48]"); // restore callee-saved flag and link registers + emitter.instruction("ldp x21, x22, [sp, #32]"); // restore callee-saved name length and cell registers + emitter.instruction("ldp x19, x20, [sp, #16]"); // restore callee-saved scope and name registers + emitter.instruction("ldp x29, x30, [sp, #0]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #96"); // release the setter frame + emitter.instruction("ret"); // return the eval status in x0 emitter.label("__elephc_eval_scope_set_fatal"); - emitter.instruction(&format!("mov x0, #{}", EVAL_STATUS_RUNTIME_FATAL)); // report invalid scope/name inputs - emitter.instruction("ret"); // return the fatal eval status without mutating scope + emitter.instruction(&format!("mov x0, #{}", EVAL_STATUS_RUNTIME_FATAL)); // report invalid scope/name inputs + emitter.instruction("ret"); // return the fatal eval status without mutating scope } /// Emits the ARM64 scope getter. fn emit_aarch64_eval_scope_get(emitter: &mut Emitter) { label_c_global(emitter, "__elephc_eval_scope_get"); - emitter.instruction("cbz x0, __elephc_eval_scope_get_fatal"); // reject null scope handles - emitter.instruction("cbz x2, __elephc_eval_scope_get_search"); // empty names do not need a readable pointer - emitter.instruction("cbz x1, __elephc_eval_scope_get_fatal"); // non-empty names must provide bytes + emitter.instruction("cbz x0, __elephc_eval_scope_get_fatal"); // reject null scope handles + emitter.instruction("cbz x2, __elephc_eval_scope_get_search"); // empty names do not need a readable pointer + emitter.instruction("cbz x1, __elephc_eval_scope_get_fatal"); // non-empty names must provide bytes emitter.label("__elephc_eval_scope_get_search"); - emitter.instruction("ldr x9, [x0]"); // load the first entry from scope->head + emitter.instruction("ldr x9, [x0]"); // load the first entry from scope->head emitter.label("__elephc_eval_scope_get_probe"); - emitter.instruction("cbz x9, __elephc_eval_scope_get_missing"); // missing names produce null cell and zero flags - emitter.instruction("ldr x10, [x9, #16]"); // load candidate name length - emitter.instruction("cmp x10, x2"); // compare candidate length with requested length - emitter.instruction("b.ne __elephc_eval_scope_get_next"); // different lengths cannot match - emitter.instruction("ldr x10, [x9, #8]"); // load candidate name bytes - emitter.instruction("mov x11, #0"); // byte index for the equality loop + emitter.instruction("cbz x9, __elephc_eval_scope_get_missing"); // missing names produce null cell and zero flags + emitter.instruction("ldr x10, [x9, #16]"); // load candidate name length + emitter.instruction("cmp x10, x2"); // compare candidate length with requested length + emitter.instruction("b.ne __elephc_eval_scope_get_next"); // different lengths cannot match + emitter.instruction("ldr x10, [x9, #8]"); // load candidate name bytes + emitter.instruction("mov x11, #0"); // byte index for the equality loop emitter.label("__elephc_eval_scope_get_cmp"); - emitter.instruction("cmp x11, x2"); // have all bytes matched? - emitter.instruction("b.eq __elephc_eval_scope_get_found"); // equal length and bytes select this entry - emitter.instruction("ldrb w12, [x10, x11]"); // load one existing name byte - emitter.instruction("ldrb w13, [x1, x11]"); // load the corresponding requested name byte - emitter.instruction("cmp w12, w13"); // compare candidate and requested name bytes - emitter.instruction("b.ne __elephc_eval_scope_get_next"); // any byte mismatch means this entry is not the target - emitter.instruction("add x11, x11, #1"); // advance to the next byte - emitter.instruction("b __elephc_eval_scope_get_cmp"); // continue comparing this candidate name + emitter.instruction("cmp x11, x2"); // have all bytes matched? + emitter.instruction("b.eq __elephc_eval_scope_get_found"); // equal length and bytes select this entry + emitter.instruction("ldrb w12, [x10, x11]"); // load one existing name byte + emitter.instruction("ldrb w13, [x1, x11]"); // load the corresponding requested name byte + emitter.instruction("cmp w12, w13"); // compare candidate and requested name bytes + emitter.instruction("b.ne __elephc_eval_scope_get_next"); // any byte mismatch means this entry is not the target + emitter.instruction("add x11, x11, #1"); // advance to the next byte + emitter.instruction("b __elephc_eval_scope_get_cmp"); // continue comparing this candidate name emitter.label("__elephc_eval_scope_get_next"); - emitter.instruction("ldr x9, [x9]"); // advance to the next entry - emitter.instruction("b __elephc_eval_scope_get_probe"); // continue scanning the scope list + emitter.instruction("ldr x9, [x9]"); // advance to the next entry + emitter.instruction("b __elephc_eval_scope_get_probe"); // continue scanning the scope list emitter.label("__elephc_eval_scope_get_found"); - emitter.instruction("cbz x3, __elephc_eval_scope_get_found_flags"); // skip cell output when caller passed null - emitter.instruction("ldr x10, [x9, #24]"); // load the visible Mixed cell pointer - emitter.instruction("str x10, [x3]"); // write the output Mixed cell pointer + emitter.instruction("cbz x3, __elephc_eval_scope_get_found_flags"); // skip cell output when caller passed null + emitter.instruction("ldr x10, [x9, #24]"); // load the visible Mixed cell pointer + emitter.instruction("str x10, [x3]"); // write the output Mixed cell pointer emitter.label("__elephc_eval_scope_get_found_flags"); - emitter.instruction("cbz x4, __elephc_eval_scope_get_ok"); // skip flags output when caller passed null - emitter.instruction("ldr w10, [x9, #32]"); // load the low ABI flag bits - emitter.instruction("str w10, [x4]"); // write the output ABI flags - emitter.instruction("b __elephc_eval_scope_get_ok"); // finish with success + emitter.instruction("cbz x4, __elephc_eval_scope_get_ok"); // skip flags output when caller passed null + emitter.instruction("ldr w10, [x9, #32]"); // load the low ABI flag bits + emitter.instruction("str w10, [x4]"); // write the output ABI flags + emitter.instruction("b __elephc_eval_scope_get_ok"); // finish with success emitter.label("__elephc_eval_scope_get_missing"); - emitter.instruction("cbz x3, __elephc_eval_scope_get_missing_flags"); // skip cell output when caller passed null - emitter.instruction("str xzr, [x3]"); // missing variables have no cell pointer + emitter.instruction("cbz x3, __elephc_eval_scope_get_missing_flags"); // skip cell output when caller passed null + emitter.instruction("str xzr, [x3]"); // missing variables have no cell pointer emitter.label("__elephc_eval_scope_get_missing_flags"); - emitter.instruction("cbz x4, __elephc_eval_scope_get_ok"); // skip flags output when caller passed null - emitter.instruction("str wzr, [x4]"); // missing variables have zero ABI flags + emitter.instruction("cbz x4, __elephc_eval_scope_get_ok"); // skip flags output when caller passed null + emitter.instruction("str wzr, [x4]"); // missing variables have zero ABI flags emitter.label("__elephc_eval_scope_get_ok"); - emitter.instruction(&format!("mov x0, #{}", EVAL_STATUS_OK)); // report successful scope lookup - emitter.instruction("ret"); // return the eval status in x0 + emitter.instruction(&format!("mov x0, #{}", EVAL_STATUS_OK)); // report successful scope lookup + emitter.instruction("ret"); // return the eval status in x0 emitter.label("__elephc_eval_scope_get_fatal"); - emitter.instruction(&format!("mov x0, #{}", EVAL_STATUS_RUNTIME_FATAL)); // report invalid scope/name inputs - emitter.instruction("ret"); // return the fatal eval status + emitter.instruction(&format!("mov x0, #{}", EVAL_STATUS_RUNTIME_FATAL)); // report invalid scope/name inputs + emitter.instruction("ret"); // return the fatal eval status } /// Emits x86_64 eval-scope helpers. `__elephc_eval_value_null` comes from the @@ -233,202 +234,201 @@ fn emit_x86_64_eval_scope_runtime(emitter: &mut Emitter) { /// Emits the x86_64 scope allocator. fn emit_x86_64_eval_scope_new(emitter: &mut Emitter) { label_c_global(emitter, "__elephc_eval_scope_new"); - emitter.instruction("push rbp"); // preserve the caller frame pointer before allocating the scope header - emitter.instruction("mov rbp, rsp"); // establish a stable frame for the nested allocator call - emitter.instruction("mov rax, 8"); // scope header stores one head pointer - emitter.instruction("call __rt_heap_alloc"); // allocate the scope header from the core heap - emitter.instruction("mov QWORD PTR [rax], 0"); // initialize the entry-list head to null - emitter.instruction("pop rbp"); // restore the caller frame pointer after allocation - emitter.instruction("ret"); // return the scope handle in rax + emitter.instruction("push rbp"); // preserve the caller frame pointer before allocating the scope header + emitter.instruction("mov rbp, rsp"); // establish a stable frame for the nested allocator call + emitter.instruction("mov rax, 8"); // scope header stores one head pointer + emitter.instruction("call __rt_heap_alloc"); // allocate the scope header from the core heap + emitter.instruction("mov QWORD PTR [rax], 0"); // initialize the entry-list head to null + emitter.instruction("pop rbp"); // restore the caller frame pointer after allocation + emitter.instruction("ret"); // return the scope handle in rax } /// Emits the x86_64 scope destructor. fn emit_x86_64_eval_scope_free(emitter: &mut Emitter) { label_c_global(emitter, "__elephc_eval_scope_free"); - emitter.instruction("test rdi, rdi"); // null scope handles are already free - emitter.instruction("jz __elephc_eval_scope_free_done"); // skip all cleanup for null handles - emitter.instruction("push rbp"); // preserve the caller frame pointer before release work - emitter.instruction("mov rbp, rsp"); // establish a stable frame for local scan state - emitter.instruction("sub rsp, 32"); // reserve scope, current-entry, and next-entry slots - emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the scope header pointer - emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the first entry in the scope list - emitter.instruction("mov QWORD PTR [rbp - 16], r10"); // store the current entry pointer + emitter.instruction("test rdi, rdi"); // null scope handles are already free + emitter.instruction("jz __elephc_eval_scope_free_done"); // skip all cleanup for null handles + emitter.instruction("push rbp"); // preserve the caller frame pointer before release work + emitter.instruction("mov rbp, rsp"); // establish a stable frame for local scan state + emitter.instruction("sub rsp, 32"); // reserve scope, current-entry, and next-entry slots + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the scope header pointer + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the first entry in the scope list + emitter.instruction("mov QWORD PTR [rbp - 16], r10"); // store the current entry pointer emitter.label("__elephc_eval_scope_free_loop"); - emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the current entry pointer - emitter.instruction("test r10, r10"); // have all entries been released? - emitter.instruction("jz __elephc_eval_scope_free_header"); // stop when the entry list is exhausted - emitter.instruction("mov r11, QWORD PTR [r10]"); // save entry->next before freeing the current entry - emitter.instruction("mov QWORD PTR [rbp - 24], r11"); // keep the next entry across release calls - emitter.instruction("mov r11, QWORD PTR [r10 + 32]"); // load the entry ABI flags - emitter.instruction(&format!("test r11, {}", EVAL_SCOPE_FLAG_OWNED)); // check whether the scope owns this Mixed cell - emitter.instruction("jz __elephc_eval_scope_free_entry"); // borrowed cells are not released by the scope - emitter.instruction("mov rax, QWORD PTR [r10 + 24]"); // load the owned Mixed cell pointer - emitter.instruction("test rax, rax"); // tolerate null owned cells defensively - emitter.instruction("jz __elephc_eval_scope_free_entry"); // skip release when the owned cell is null - emitter.instruction("call __rt_decref_mixed"); // release the scope-owned Mixed cell + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the current entry pointer + emitter.instruction("test r10, r10"); // have all entries been released? + emitter.instruction("jz __elephc_eval_scope_free_header"); // stop when the entry list is exhausted + emitter.instruction("mov r11, QWORD PTR [r10]"); // save entry->next before freeing the current entry + emitter.instruction("mov QWORD PTR [rbp - 24], r11"); // keep the next entry across release calls + emitter.instruction("mov r11, QWORD PTR [r10 + 32]"); // load the entry ABI flags + emitter.instruction(&format!("test r11, {}", EVAL_SCOPE_FLAG_OWNED)); // check whether the scope owns this Mixed cell + emitter.instruction("jz __elephc_eval_scope_free_entry"); // borrowed cells are not released by the scope + emitter.instruction("mov rax, QWORD PTR [r10 + 24]"); // load the owned Mixed cell pointer + emitter.instruction("test rax, rax"); // tolerate null owned cells defensively + emitter.instruction("jz __elephc_eval_scope_free_entry"); // skip release when the owned cell is null + emitter.instruction("call __rt_decref_mixed"); // release the scope-owned Mixed cell emitter.label("__elephc_eval_scope_free_entry"); - emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // pass the current entry allocation to the heap free helper - emitter.instruction("call __rt_heap_free"); // release the entry record itself - emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the saved next entry - emitter.instruction("mov QWORD PTR [rbp - 16], r10"); // make the saved next entry current - emitter.instruction("jmp __elephc_eval_scope_free_loop"); // continue freeing entries + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // pass the current entry allocation to the heap free helper + emitter.instruction("call __rt_heap_free"); // release the entry record itself + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the saved next entry + emitter.instruction("mov QWORD PTR [rbp - 16], r10"); // make the saved next entry current + emitter.instruction("jmp __elephc_eval_scope_free_loop"); // continue freeing entries emitter.label("__elephc_eval_scope_free_header"); - emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // pass the scope header allocation to the heap free helper - emitter.instruction("call __rt_heap_free"); // release the scope header after all entries - emitter.instruction("add rsp, 32"); // release local scan slots - emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // pass the scope header allocation to the heap free helper + emitter.instruction("call __rt_heap_free"); // release the scope header after all entries + emitter.instruction("add rsp, 32"); // release local scan slots + emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.label("__elephc_eval_scope_free_done"); - emitter.instruction("ret"); // return after the scope is fully released + emitter.instruction("ret"); // return after the scope is fully released } /// Emits the x86_64 scope setter. fn emit_x86_64_eval_scope_set(emitter: &mut Emitter) { label_c_global(emitter, "__elephc_eval_scope_set"); - emitter.instruction("test rdi, rdi"); // reject null scope handles - emitter.instruction("jz __elephc_eval_scope_set_fatal"); // return a runtime-fatal status for null scope - emitter.instruction("test rdx, rdx"); // empty names do not need a readable pointer - emitter.instruction("jz __elephc_eval_scope_set_frame"); // skip the pointer check for empty names - emitter.instruction("test rsi, rsi"); // non-empty names must provide bytes - emitter.instruction("jz __elephc_eval_scope_set_fatal"); // return a runtime-fatal status for invalid names + emitter.instruction("test rdi, rdi"); // reject null scope handles + emitter.instruction("jz __elephc_eval_scope_set_fatal"); // return a runtime-fatal status for null scope + emitter.instruction("test rdx, rdx"); // empty names do not need a readable pointer + emitter.instruction("jz __elephc_eval_scope_set_frame"); // skip the pointer check for empty names + emitter.instruction("test rsi, rsi"); // non-empty names must provide bytes + emitter.instruction("jz __elephc_eval_scope_set_fatal"); // return a runtime-fatal status for invalid names emitter.label("__elephc_eval_scope_set_frame"); - emitter.instruction("push rbp"); // preserve the caller frame pointer before scope mutation - emitter.instruction("mov rbp, rsp"); // establish a stable frame for saved inputs and scan state - emitter.instruction("sub rsp, 64"); // reserve scope, name, length, cell, flags, previous-next, current, and index slots - emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save scope header pointer - emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save name byte pointer - emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save name byte length - emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save Mixed cell pointer - emitter.instruction("mov QWORD PTR [rbp - 40], r8"); // save caller-provided ABI flags - emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // previous-next pointer initially addresses scope->head - emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the first candidate entry - emitter.instruction("mov QWORD PTR [rbp - 56], r10"); // save the current candidate entry + emitter.instruction("push rbp"); // preserve the caller frame pointer before scope mutation + emitter.instruction("mov rbp, rsp"); // establish a stable frame for saved inputs and scan state + emitter.instruction("sub rsp, 64"); // reserve scope, name, length, cell, flags, previous-next, current, and index slots + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save scope header pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save name byte pointer + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save name byte length + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save Mixed cell pointer + emitter.instruction("mov QWORD PTR [rbp - 40], r8"); // save caller-provided ABI flags + emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // previous-next pointer initially addresses scope->head + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the first candidate entry + emitter.instruction("mov QWORD PTR [rbp - 56], r10"); // save the current candidate entry emitter.label("__elephc_eval_scope_set_probe"); - emitter.instruction("mov r9, QWORD PTR [rbp - 56]"); // reload the current candidate entry - emitter.instruction("test r9, r9"); // is the requested name absent? - emitter.instruction("jz __elephc_eval_scope_set_insert"); // insert when the scan reaches the end - emitter.instruction("mov r10, QWORD PTR [r9 + 16]"); // load candidate name length - emitter.instruction("cmp r10, QWORD PTR [rbp - 24]"); // compare candidate length with requested length - emitter.instruction("jne __elephc_eval_scope_set_next"); // different lengths cannot match - emitter.instruction("mov r10, QWORD PTR [r9 + 8]"); // load candidate name bytes - emitter.instruction("xor r11, r11"); // byte index for the equality loop + emitter.instruction("mov r9, QWORD PTR [rbp - 56]"); // reload the current candidate entry + emitter.instruction("test r9, r9"); // is the requested name absent? + emitter.instruction("jz __elephc_eval_scope_set_insert"); // insert when the scan reaches the end + emitter.instruction("mov r10, QWORD PTR [r9 + 16]"); // load candidate name length + emitter.instruction("cmp r10, QWORD PTR [rbp - 24]"); // compare candidate length with requested length + emitter.instruction("jne __elephc_eval_scope_set_next"); // different lengths cannot match + emitter.instruction("mov r10, QWORD PTR [r9 + 8]"); // load candidate name bytes + emitter.instruction("xor r11, r11"); // byte index for the equality loop emitter.label("__elephc_eval_scope_set_cmp"); - emitter.instruction("cmp r11, QWORD PTR [rbp - 24]"); // have all bytes matched? - emitter.instruction("je __elephc_eval_scope_set_update"); // equal length and bytes select this entry - emitter.instruction("mov al, BYTE PTR [r10 + r11]"); // load one existing name byte - emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // reload requested name pointer - emitter.instruction("cmp al, BYTE PTR [r8 + r11]"); // compare candidate and requested name bytes - emitter.instruction("jne __elephc_eval_scope_set_next"); // any byte mismatch means this entry is not the target - emitter.instruction("add r11, 1"); // advance to the next byte - emitter.instruction("jmp __elephc_eval_scope_set_cmp"); // continue comparing this candidate name + emitter.instruction("cmp r11, QWORD PTR [rbp - 24]"); // have all bytes matched? + emitter.instruction("je __elephc_eval_scope_set_update"); // equal length and bytes select this entry + emitter.instruction("mov al, BYTE PTR [r10 + r11]"); // load one existing name byte + emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // reload requested name pointer + emitter.instruction("cmp al, BYTE PTR [r8 + r11]"); // compare candidate and requested name bytes + emitter.instruction("jne __elephc_eval_scope_set_next"); // any byte mismatch means this entry is not the target + emitter.instruction("add r11, 1"); // advance to the next byte + emitter.instruction("jmp __elephc_eval_scope_set_cmp"); // continue comparing this candidate name emitter.label("__elephc_eval_scope_set_next"); - emitter.instruction("mov QWORD PTR [rbp - 48], r9"); // previous-next pointer now addresses current->next - emitter.instruction("mov r10, QWORD PTR [r9]"); // advance to the next entry - emitter.instruction("mov QWORD PTR [rbp - 56], r10"); // save the next candidate as current - emitter.instruction("jmp __elephc_eval_scope_set_probe"); // continue scanning the entry list + emitter.instruction("mov QWORD PTR [rbp - 48], r9"); // previous-next pointer now addresses current->next + emitter.instruction("mov r10, QWORD PTR [r9]"); // advance to the next entry + emitter.instruction("mov QWORD PTR [rbp - 56], r10"); // save the next candidate as current + emitter.instruction("jmp __elephc_eval_scope_set_probe"); // continue scanning the entry list emitter.label("__elephc_eval_scope_set_update"); - emitter.instruction("mov r10, QWORD PTR [r9 + 32]"); // load old entry flags - emitter.instruction(&format!("test r10, {}", EVAL_SCOPE_FLAG_OWNED)); // check whether the old cell is scope-owned - emitter.instruction("jz __elephc_eval_scope_set_store"); // borrowed old cells do not need release - emitter.instruction("mov rax, QWORD PTR [r9 + 24]"); // load the old Mixed cell pointer - emitter.instruction("cmp rax, QWORD PTR [rbp - 32]"); // compare old and replacement cells - emitter.instruction("je __elephc_eval_scope_set_store"); // retaining the same cell must not decref it - emitter.instruction("test rax, rax"); // tolerate null old cells defensively - emitter.instruction("jz __elephc_eval_scope_set_store"); // skip release when the old cell is null - emitter.instruction("call __rt_decref_mixed"); // release the overwritten owned Mixed cell - emitter.instruction("mov r9, QWORD PTR [rbp - 56]"); // reload current entry after the runtime call + emitter.instruction("mov r10, QWORD PTR [r9 + 32]"); // load old entry flags + emitter.instruction(&format!("test r10, {}", EVAL_SCOPE_FLAG_OWNED)); // check whether the old cell is scope-owned + emitter.instruction("jz __elephc_eval_scope_set_store"); // borrowed old cells do not need release + emitter.instruction("mov rax, QWORD PTR [r9 + 24]"); // load the old Mixed cell pointer + emitter.instruction("cmp rax, QWORD PTR [rbp - 32]"); // compare old and replacement cells + emitter.instruction("je __elephc_eval_scope_set_store"); // retaining the same cell must not decref it + emitter.instruction("test rax, rax"); // tolerate null old cells defensively + emitter.instruction("jz __elephc_eval_scope_set_store"); // skip release when the old cell is null + emitter.instruction("call __rt_decref_mixed"); // release the overwritten owned Mixed cell + emitter.instruction("mov r9, QWORD PTR [rbp - 56]"); // reload current entry after the runtime call emitter.label("__elephc_eval_scope_set_store"); - emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the replacement Mixed cell - emitter.instruction("mov QWORD PTR [r9 + 24], r10"); // store the replacement Mixed cell - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload caller-provided ABI flags + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the replacement Mixed cell + emitter.instruction("mov QWORD PTR [r9 + 24], r10"); // store the replacement Mixed cell + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload caller-provided ABI flags emitter.instruction(&format!("or r10, {}", EVAL_SCOPE_FLAG_PRESENT | EVAL_SCOPE_FLAG_DIRTY)); // mark the entry visible and dirty - emitter.instruction("mov QWORD PTR [r9 + 32], r10"); // publish the updated ABI flags - emitter.instruction(&format!("mov eax, {}", EVAL_STATUS_OK)); // report successful scope write - emitter.instruction("jmp __elephc_eval_scope_set_done"); // restore the frame and return + emitter.instruction("mov QWORD PTR [r9 + 32], r10"); // publish the updated ABI flags + emitter.instruction(&format!("mov eax, {}", EVAL_STATUS_OK)); // report successful scope write + emitter.instruction("jmp __elephc_eval_scope_set_done"); // restore the frame and return emitter.label("__elephc_eval_scope_set_insert"); - emitter.instruction("mov rax, 40"); // entry records store next, name, length, cell, and flags - emitter.instruction("call __rt_heap_alloc"); // allocate a new entry record - emitter.instruction("mov r9, QWORD PTR [rbp - 56]"); // reload old successor, normally null - emitter.instruction("mov QWORD PTR [rax], r9"); // new entry next points at the old successor - emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload borrowed static name pointer - emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the borrowed static name pointer - emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload name byte length - emitter.instruction("mov QWORD PTR [rax + 16], r10"); // store the name byte length - emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload Mixed cell pointer - emitter.instruction("mov QWORD PTR [rax + 24], r10"); // store the Mixed cell pointer - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload caller-provided ABI flags + emitter.instruction("mov rax, 40"); // entry records store next, name, length, cell, and flags + emitter.instruction("call __rt_heap_alloc"); // allocate a new entry record + emitter.instruction("mov r9, QWORD PTR [rbp - 56]"); // reload old successor, normally null + emitter.instruction("mov QWORD PTR [rax], r9"); // new entry next points at the old successor + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload borrowed static name pointer + emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the borrowed static name pointer + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload name byte length + emitter.instruction("mov QWORD PTR [rax + 16], r10"); // store the name byte length + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload Mixed cell pointer + emitter.instruction("mov QWORD PTR [rax + 24], r10"); // store the Mixed cell pointer + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload caller-provided ABI flags emitter.instruction(&format!("or r10, {}", EVAL_SCOPE_FLAG_PRESENT | EVAL_SCOPE_FLAG_DIRTY)); // mark the new entry visible and dirty - emitter.instruction("mov QWORD PTR [rax + 32], r10"); // store the new entry flags - emitter.instruction("mov r11, QWORD PTR [rbp - 48]"); // reload the previous-next pointer - emitter.instruction("mov QWORD PTR [r11], rax"); // link the new entry through the previous-next pointer - emitter.instruction(&format!("mov eax, {}", EVAL_STATUS_OK)); // report successful scope write + emitter.instruction("mov QWORD PTR [rax + 32], r10"); // store the new entry flags + emitter.instruction("mov r11, QWORD PTR [rbp - 48]"); // reload the previous-next pointer + emitter.instruction("mov QWORD PTR [r11], rax"); // link the new entry through the previous-next pointer + emitter.instruction(&format!("mov eax, {}", EVAL_STATUS_OK)); // report successful scope write emitter.label("__elephc_eval_scope_set_done"); - emitter.instruction("add rsp, 64"); // release saved input and scan slots - emitter.instruction("pop rbp"); // restore the caller frame pointer - emitter.instruction("ret"); // return the eval status in rax + emitter.instruction("add rsp, 64"); // release saved input and scan slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the eval status in rax emitter.label("__elephc_eval_scope_set_fatal"); - emitter.instruction(&format!("mov eax, {}", EVAL_STATUS_RUNTIME_FATAL)); // report invalid scope/name inputs - emitter.instruction("ret"); // return the fatal eval status without mutating scope + emitter.instruction(&format!("mov eax, {}", EVAL_STATUS_RUNTIME_FATAL)); // report invalid scope/name inputs + emitter.instruction("ret"); // return the fatal eval status without mutating scope } /// Emits the x86_64 scope getter. fn emit_x86_64_eval_scope_get(emitter: &mut Emitter) { label_c_global(emitter, "__elephc_eval_scope_get"); - emitter.instruction("test rdi, rdi"); // reject null scope handles - emitter.instruction("jz __elephc_eval_scope_get_fatal"); // return a runtime-fatal status for null scope - emitter.instruction("test rdx, rdx"); // empty names do not need a readable pointer - emitter.instruction("jz __elephc_eval_scope_get_search"); // skip the pointer check for empty names - emitter.instruction("test rsi, rsi"); // non-empty names must provide bytes - emitter.instruction("jz __elephc_eval_scope_get_fatal"); // return a runtime-fatal status for invalid names + emitter.instruction("test rdi, rdi"); // reject null scope handles + emitter.instruction("jz __elephc_eval_scope_get_fatal"); // return a runtime-fatal status for null scope + emitter.instruction("test rdx, rdx"); // empty names do not need a readable pointer + emitter.instruction("jz __elephc_eval_scope_get_search"); // skip the pointer check for empty names + emitter.instruction("test rsi, rsi"); // non-empty names must provide bytes + emitter.instruction("jz __elephc_eval_scope_get_fatal"); // return a runtime-fatal status for invalid names emitter.label("__elephc_eval_scope_get_search"); - emitter.instruction("mov r9, QWORD PTR [rdi]"); // load the first entry from scope->head + emitter.instruction("mov r9, QWORD PTR [rdi]"); // load the first entry from scope->head emitter.label("__elephc_eval_scope_get_probe"); - emitter.instruction("test r9, r9"); // has the scan reached the end of the entry list? - emitter.instruction("jz __elephc_eval_scope_get_missing"); // missing names produce null cell and zero flags - emitter.instruction("mov r10, QWORD PTR [r9 + 16]"); // load candidate name length - emitter.instruction("cmp r10, rdx"); // compare candidate length with requested length - emitter.instruction("jne __elephc_eval_scope_get_next"); // different lengths cannot match - emitter.instruction("mov r10, QWORD PTR [r9 + 8]"); // load candidate name bytes - emitter.instruction("xor r11, r11"); // byte index for the equality loop + emitter.instruction("test r9, r9"); // has the scan reached the end of the entry list? + emitter.instruction("jz __elephc_eval_scope_get_missing"); // missing names produce null cell and zero flags + emitter.instruction("mov r10, QWORD PTR [r9 + 16]"); // load candidate name length + emitter.instruction("cmp r10, rdx"); // compare candidate length with requested length + emitter.instruction("jne __elephc_eval_scope_get_next"); // different lengths cannot match + emitter.instruction("mov r10, QWORD PTR [r9 + 8]"); // load candidate name bytes + emitter.instruction("xor r11, r11"); // byte index for the equality loop emitter.label("__elephc_eval_scope_get_cmp"); - emitter.instruction("cmp r11, rdx"); // have all bytes matched? - emitter.instruction("je __elephc_eval_scope_get_found"); // equal length and bytes select this entry - emitter.instruction("mov al, BYTE PTR [r10 + r11]"); // load one existing name byte - emitter.instruction("cmp al, BYTE PTR [rsi + r11]"); // compare candidate and requested name bytes - emitter.instruction("jne __elephc_eval_scope_get_next"); // any byte mismatch means this entry is not the target - emitter.instruction("add r11, 1"); // advance to the next byte - emitter.instruction("jmp __elephc_eval_scope_get_cmp"); // continue comparing this candidate name + emitter.instruction("cmp r11, rdx"); // have all bytes matched? + emitter.instruction("je __elephc_eval_scope_get_found"); // equal length and bytes select this entry + emitter.instruction("mov al, BYTE PTR [r10 + r11]"); // load one existing name byte + emitter.instruction("cmp al, BYTE PTR [rsi + r11]"); // compare candidate and requested name bytes + emitter.instruction("jne __elephc_eval_scope_get_next"); // any byte mismatch means this entry is not the target + emitter.instruction("add r11, 1"); // advance to the next byte + emitter.instruction("jmp __elephc_eval_scope_get_cmp"); // continue comparing this candidate name emitter.label("__elephc_eval_scope_get_next"); - emitter.instruction("mov r9, QWORD PTR [r9]"); // advance to the next entry - emitter.instruction("jmp __elephc_eval_scope_get_probe"); // continue scanning the scope list + emitter.instruction("mov r9, QWORD PTR [r9]"); // advance to the next entry + emitter.instruction("jmp __elephc_eval_scope_get_probe"); // continue scanning the scope list emitter.label("__elephc_eval_scope_get_found"); - emitter.instruction("test rcx, rcx"); // did the caller request cell output? - emitter.instruction("jz __elephc_eval_scope_get_found_flags"); // skip cell output for null out_cell - emitter.instruction("mov r10, QWORD PTR [r9 + 24]"); // load the visible Mixed cell pointer - emitter.instruction("mov QWORD PTR [rcx], r10"); // write the output Mixed cell pointer + emitter.instruction("test rcx, rcx"); // did the caller request cell output? + emitter.instruction("jz __elephc_eval_scope_get_found_flags"); // skip cell output for null out_cell + emitter.instruction("mov r10, QWORD PTR [r9 + 24]"); // load the visible Mixed cell pointer + emitter.instruction("mov QWORD PTR [rcx], r10"); // write the output Mixed cell pointer emitter.label("__elephc_eval_scope_get_found_flags"); - emitter.instruction("test r8, r8"); // did the caller request flag output? - emitter.instruction("jz __elephc_eval_scope_get_ok"); // skip flags output for null out_flags - emitter.instruction("mov r10d, DWORD PTR [r9 + 32]"); // load the low ABI flag bits - emitter.instruction("mov DWORD PTR [r8], r10d"); // write the output ABI flags - emitter.instruction("jmp __elephc_eval_scope_get_ok"); // finish with success + emitter.instruction("test r8, r8"); // did the caller request flag output? + emitter.instruction("jz __elephc_eval_scope_get_ok"); // skip flags output for null out_flags + emitter.instruction("mov r10d, DWORD PTR [r9 + 32]"); // load the low ABI flag bits + emitter.instruction("mov DWORD PTR [r8], r10d"); // write the output ABI flags + emitter.instruction("jmp __elephc_eval_scope_get_ok"); // finish with success emitter.label("__elephc_eval_scope_get_missing"); - emitter.instruction("test rcx, rcx"); // did the caller request cell output? - emitter.instruction("jz __elephc_eval_scope_get_missing_flags"); // skip cell output for null out_cell - emitter.instruction("mov QWORD PTR [rcx], 0"); // missing variables have no cell pointer + emitter.instruction("test rcx, rcx"); // did the caller request cell output? + emitter.instruction("jz __elephc_eval_scope_get_missing_flags"); // skip cell output for null out_cell + emitter.instruction("mov QWORD PTR [rcx], 0"); // missing variables have no cell pointer emitter.label("__elephc_eval_scope_get_missing_flags"); - emitter.instruction("test r8, r8"); // did the caller request flag output? - emitter.instruction("jz __elephc_eval_scope_get_ok"); // skip flags output for null out_flags - emitter.instruction("mov DWORD PTR [r8], 0"); // missing variables have zero ABI flags + emitter.instruction("test r8, r8"); // did the caller request flag output? + emitter.instruction("jz __elephc_eval_scope_get_ok"); // skip flags output for null out_flags + emitter.instruction("mov DWORD PTR [r8], 0"); // missing variables have zero ABI flags emitter.label("__elephc_eval_scope_get_ok"); - emitter.instruction(&format!("mov eax, {}", EVAL_STATUS_OK)); // report successful scope lookup - emitter.instruction("ret"); // return the eval status in rax + emitter.instruction(&format!("mov eax, {}", EVAL_STATUS_OK)); // report successful scope lookup + emitter.instruction("ret"); // return the eval status in rax emitter.label("__elephc_eval_scope_get_fatal"); - emitter.instruction(&format!("mov eax, {}", EVAL_STATUS_RUNTIME_FATAL)); // report invalid scope/name inputs - emitter.instruction("ret"); // return the fatal eval status + emitter.instruction(&format!("mov eax, {}", EVAL_STATUS_RUNTIME_FATAL)); // report invalid scope/name inputs + emitter.instruction("ret"); // return the fatal eval status } /// Emits a global label with platform C-symbol mangling. fn label_c_global(emitter: &mut Emitter, name: &str) { - let symbol = emitter.target.extern_symbol(name); - emitter.label_global(&symbol); + abi::emit_c_callback_entry(emitter, name); } diff --git a/src/codegen_support/runtime/exceptions.rs b/src/codegen_support/runtime/exceptions.rs index aeb8628e7b..79cea6f34e 100644 --- a/src/codegen_support/runtime/exceptions.rs +++ b/src/codegen_support/runtime/exceptions.rs @@ -13,6 +13,8 @@ mod class_implements; mod dynamic_instanceof; mod matches; mod rethrow_current; +mod setjmp; +mod static_throw; mod throw_current; pub use class_implements::emit_class_implements_interface; @@ -20,4 +22,6 @@ pub use cleanup_frames::emit_exception_cleanup_frames; pub use dynamic_instanceof::emit_dynamic_instanceof; pub use matches::emit_exception_matches; pub use rethrow_current::emit_rethrow_current; +pub use setjmp::emit_setjmp_longjmp; +pub use static_throw::emit_throw_static_exception; pub use throw_current::emit_throw_current; diff --git a/src/codegen_support/runtime/exceptions/cleanup_frames.rs b/src/codegen_support/runtime/exceptions/cleanup_frames.rs index 91f8425217..06178b60ba 100644 --- a/src/codegen_support/runtime/exceptions/cleanup_frames.rs +++ b/src/codegen_support/runtime/exceptions/cleanup_frames.rs @@ -81,7 +81,7 @@ fn emit_exception_cleanup_frames_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("test r10, r10"); // does this activation record have cleanup work to run? emitter.instruction("je __rt_exception_cleanup_frames_next"); // skip callback execution when the activation carries no cleanup hook emitter.instruction("mov rdi, r11"); // pass the unwound activation frame pointer into the cleanup callback ABI register - emitter.instruction("call r10"); // run the per-function cleanup callback for this unwound activation record + emitter.emit_platform_callback_call("r10", 1); emitter.label("__rt_exception_cleanup_frames_next"); emitter.instruction("mov r13, QWORD PTR [r13]"); // advance to the previous activation record in the cleanup stack diff --git a/src/codegen_support/runtime/exceptions/setjmp.rs b/src/codegen_support/runtime/exceptions/setjmp.rs new file mode 100644 index 0000000000..7850c63fe9 --- /dev/null +++ b/src/codegen_support/runtime/exceptions/setjmp.rs @@ -0,0 +1,125 @@ +//! Purpose: +//! Emits elephc's own `__rt_setjmp` / `__rt_longjmp` runtime primitives for the +//! Windows x86_64 (PE32+) target — a plain callee-saved-register save/restore with +//! NO structured-exception-handling (SEH) involvement. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` on Windows x86_64 only. +//! +//! Key details: +//! - MinGW's C-library `setjmp`/`longjmp` on x86_64 Windows are SEH-based: `longjmp` +//! unwinds via `RtlUnwindEx` and `setjmp` walks the stack to capture the SEH +//! establisher frame. That walk runs off the top of elephc's small hand-rolled +//! Fiber stacks (write access-violation at `stack_top`), and the MinGW entry points +//! read their arguments in the MSx64 registers (`rcx`/`rdx`) while elephc's runtime +//! passes them SysV-style (`rdi`/`rsi`). Both problems break every `try`/`catch` +//! and every Fiber/Generator on Windows. +//! - elephc's exception model does not need Windows SEH: `setjmp` marks a resume +//! point and `__rt_throw_current` `longjmp`s back to it after running its own frame +//! cleanup. A plain register save/restore is therefore both sufficient and correct, +//! and it works on any stack (main thread or Fiber) because it never walks frames or +//! touches the TEB. These primitives take their arguments SysV-style (`rdi` = +//! `jmp_buf`, `rsi` = value) exactly as elephc's `bl_c("setjmp"/"longjmp")` call +//! sites already stage them, so no per-site ABI shuffle is required — `bl_c` simply +//! routes `setjmp`/`longjmp` to these labels on the Windows x86_64 target. + +use crate::codegen_support::emit::Emitter; + +/// Emits `__rt_setjmp` and `__rt_longjmp`, elephc's SEH-free setjmp/longjmp pair for +/// Windows x86_64. Only emitted on the Windows x86_64 target; on every other target +/// the C-library `setjmp`/`longjmp` are used unchanged. +/// +/// `__rt_setjmp` takes `rdi` = `jmp_buf` pointer and returns 0 in `eax`. It saves the +/// callee-saved registers, the caller's stack pointer, and the return address into the +/// 64-byte `jmp_buf` so a later `__rt_longjmp` can resume exactly where `__rt_setjmp` +/// returned. +/// +/// `__rt_longjmp` takes `rdi` = `jmp_buf` pointer and `rsi` = return value. It restores +/// the saved registers and stack pointer, then jumps to the saved return address with +/// `eax` = the requested value (normalised to 1 when the caller passed 0, matching C +/// `longjmp` semantics). +/// +/// jmp_buf layout (64 bytes, fits inside the 200-byte handler jmp_buf slot): +/// `+0` rbx, `+8` rbp, `+16` r12, `+24` r13, `+32` r14, `+40` r15, `+48` caller rsp, +/// `+56` return address. +pub fn emit_setjmp_longjmp(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: setjmp (SEH-free, Windows x86_64) ---"); + emitter.label_global("__rt_setjmp"); + + emitter.instruction("mov QWORD PTR [rdi], rbx"); // jmp_buf+0 = rbx (callee-saved) + emitter.instruction("mov QWORD PTR [rdi + 8], rbp"); // jmp_buf+8 = rbp (frame pointer) + emitter.instruction("mov QWORD PTR [rdi + 16], r12"); // jmp_buf+16 = r12 (callee-saved) + emitter.instruction("mov QWORD PTR [rdi + 24], r13"); // jmp_buf+24 = r13 (callee-saved) + emitter.instruction("mov QWORD PTR [rdi + 32], r14"); // jmp_buf+32 = r14 (callee-saved) + emitter.instruction("mov QWORD PTR [rdi + 40], r15"); // jmp_buf+40 = r15 (callee-saved) + emitter.instruction("lea rax, [rsp + 8]"); // rax = caller's rsp (past the pushed return address) + emitter.instruction("mov QWORD PTR [rdi + 48], rax"); // jmp_buf+48 = caller stack pointer + emitter.instruction("mov rax, QWORD PTR [rsp]"); // rax = return address left by the call into __rt_setjmp + emitter.instruction("mov QWORD PTR [rdi + 56], rax"); // jmp_buf+56 = resume address + emitter.instruction("xor eax, eax"); // setjmp returns 0 on the direct path + emitter.instruction("ret"); // resume the caller with a zero result + + emitter.blank(); + emitter.comment("--- runtime: longjmp (SEH-free, Windows x86_64) ---"); + emitter.label_global("__rt_longjmp"); + + emitter.instruction("mov rbx, QWORD PTR [rdi]"); // restore rbx from jmp_buf+0 + emitter.instruction("mov rbp, QWORD PTR [rdi + 8]"); // restore rbp from jmp_buf+8 + emitter.instruction("mov r12, QWORD PTR [rdi + 16]"); // restore r12 from jmp_buf+16 + emitter.instruction("mov r13, QWORD PTR [rdi + 24]"); // restore r13 from jmp_buf+24 + emitter.instruction("mov r14, QWORD PTR [rdi + 32]"); // restore r14 from jmp_buf+32 + emitter.instruction("mov r15, QWORD PTR [rdi + 40]"); // restore r15 from jmp_buf+40 + emitter.instruction("mov rdx, QWORD PTR [rdi + 56]"); // rdx = saved resume address (before rsp is switched) + emitter.instruction("mov rsp, QWORD PTR [rdi + 48]"); // adopt the saved caller stack pointer + emitter.instruction("mov eax, esi"); // eax = requested longjmp return value + emitter.instruction("test eax, eax"); // was the requested value zero? + emitter.instruction("jne __rt_longjmp_resume"); // non-zero values pass through unchanged + emitter.instruction("mov eax, 1"); // C longjmp normalises a zero value to 1 + emitter.label("__rt_longjmp_resume"); + emitter.instruction("jmp rdx"); // resume execution at the saved __rt_setjmp return point +} + +#[cfg(test)] +mod tests { + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::*; + + /// Verifies `__rt_setjmp`/`__rt_longjmp` are emitted with the plain register + /// save/restore body and the SysV `rdi` jmp_buf pointer convention. + #[test] + fn test_emits_plain_setjmp_longjmp() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_setjmp_longjmp(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("__rt_setjmp"), "missing __rt_setjmp label"); + assert!(asm.contains("__rt_longjmp"), "missing __rt_longjmp label"); + assert!(asm.contains("mov QWORD PTR [rdi], rbx")); + assert!(asm.contains("mov QWORD PTR [rdi + 48], rax")); + assert!(asm.contains("jmp rdx")); + assert!(!asm.contains("call"), "the SEH-free primitives must be leaf helpers"); + } + + /// Verifies `bl_c` routes `setjmp`/`longjmp` to the SEH-free helpers on the + /// Windows x86_64 target while leaving the C-library names untouched elsewhere, + /// so non-Windows codegen stays byte-identical. + #[test] + fn test_bl_c_routes_setjmp_longjmp_on_windows_only() { + let mut win = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + win.bl_c("setjmp"); + win.bl_c("longjmp"); + let win_asm = win.output(); + assert!(win_asm.contains("call __rt_setjmp")); + assert!(win_asm.contains("call __rt_longjmp")); + + let mut linux = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + linux.bl_c("setjmp"); + linux.bl_c("longjmp"); + let linux_asm = linux.output(); + assert!(linux_asm.contains("call setjmp")); + assert!(linux_asm.contains("call longjmp")); + assert!(!linux_asm.contains("__rt_setjmp")); + assert!(!linux_asm.contains("__rt_longjmp")); + } +} diff --git a/src/codegen_support/runtime/exceptions/static_throw.rs b/src/codegen_support/runtime/exceptions/static_throw.rs new file mode 100644 index 0000000000..7266b8798b --- /dev/null +++ b/src/codegen_support/runtime/exceptions/static_throw.rs @@ -0,0 +1,72 @@ +//! Purpose: +//! Emits the shared x86_64 static-message Throwable allocator used by runtime error paths. +//! Keeps alternative throw branches out of their caller's unwind range. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()`. +//! +//! Key details: +//! - Inputs use the internal SysV convention: `rdi` class id, `rsi` message pointer, `rdx` length. +//! - The helper never returns; it publishes `_exc_value` and tail-calls `__rt_throw_current`. + +use crate::codegen_support::abi; +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// Emits the shared x86_64 static-message Throwable allocation helper. +pub fn emit_throw_static_exception(emitter: &mut Emitter) { + if emitter.target.arch != Arch::X86_64 { + return; + } + emitter.label_global("__rt_throw_static_exception"); + emitter.instruction("push rbp"); // preserve the caller frame pointer while allocating + emitter.instruction("mov rbp, rsp"); // establish the shared throw-helper frame + emitter.instruction("sub rsp, 32"); // reserve aligned class, message, and length spills + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the exception class id + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // preserve the static message pointer + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // preserve the static message length + emitter.instruction("mov rax, 56"); // request Throwable payload storage (message/code/previous) + emitter.instruction("call __rt_heap_alloc"); // allocate the exception object payload + emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(6))); // stamp the canonical x86_64 throwable heap kind + emitter.instruction("mov QWORD PTR [rax - 8], r10"); // publish the allocation kind before unwinding + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the exception class id + emitter.instruction("mov QWORD PTR [rax], r10"); // store the class id in the object header + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the static message pointer + emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the exception message pointer + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the static message length + emitter.instruction("mov QWORD PTR [rax + 16], r10"); // store the exception message length + emitter.instruction("mov QWORD PTR [rax + 24], 0"); // default the exception code to zero + emitter.instruction("mov QWORD PTR [rax + 40], 0"); // default the previous Throwable to null + abi::emit_store_reg_to_symbol(emitter, "rax", "_exc_value", 0); // publish the active Throwable + emitter.instruction("mov rsp, rbp"); // release the shared throw-helper frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("jmp __rt_throw_current"); // enter the standard exception unwinder + emitter.blank(); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::codegen_support::platform::{Platform, Target}; + + /// Verifies the shared x86_64 throw helper uses the canonical object header and full layout. + #[test] + fn x86_64_static_throw_uses_canonical_throwable_layout() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_throw_static_exception(&mut emitter); + let asm = emitter.output(); + let kind = crate::codegen_support::sentinels::x86_64_heap_kind_word(6); + assert!(asm.contains("mov rax, 56")); + assert!(asm.contains(&format!("mov r10, 0x{kind:x}"))); + assert!(asm.contains("mov QWORD PTR [rax + 40], 0")); + assert!(asm.contains("jmp __rt_throw_current")); + } + + /// Verifies ARM64 keeps using its target-specific in-place throw paths. + #[test] + fn aarch64_does_not_emit_x86_64_static_throw_helper() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::AArch64)); + emit_throw_static_exception(&mut emitter); + assert!(emitter.output().is_empty()); + } +} diff --git a/src/codegen_support/runtime/fibers/alloc.rs b/src/codegen_support/runtime/fibers/alloc.rs index 0ce4482591..c9f3b2af2b 100644 --- a/src/codegen_support/runtime/fibers/alloc.rs +++ b/src/codegen_support/runtime/fibers/alloc.rs @@ -15,7 +15,7 @@ use crate::codegen_support::platform::{Arch, Platform}; /// stack. Set to 16 KB so the guard fully covers a single page on every /// supported target — macOS aarch64 uses 16 KB pages, Linux aarch64 typically /// uses 4 KB but accepts oversized protection ranges silently. -const FIBER_GUARD_PAGE_SIZE: i32 = 16384; +pub(super) const FIBER_GUARD_PAGE_SIZE: i32 = 16384; /// Returns the platform-specific flag word for `MAP_PRIVATE | MAP_ANONYMOUS`. /// - macOS: `0x1002` (MAP_PRIVATE | MAP_ANON) @@ -24,7 +24,7 @@ fn map_anon_private_flags(platform: Platform) -> i32 { match platform { Platform::MacOS => 0x1002, // MAP_PRIVATE | MAP_ANON Platform::Linux => 0x22, // MAP_PRIVATE | MAP_ANONYMOUS - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 0x22, // MAP_PRIVATE | MAP_ANONYMOUS — ignored by __rt_sys_mmap/VirtualAlloc shim } } diff --git a/src/codegen_support/runtime/fibers/api/arm64.rs b/src/codegen_support/runtime/fibers/api/arm64.rs index efa8cf1b2d..a03069c447 100644 --- a/src/codegen_support/runtime/fibers/api/arm64.rs +++ b/src/codegen_support/runtime/fibers/api/arm64.rs @@ -62,8 +62,8 @@ pub(super) fn emit_throw_state_error(emitter: &mut Emitter) { /// x2 = wrapper entry that adapts Fiber Mixed traffic to the callable ABI /// Output: x0 = pointer to the new Fiber object (16-byte heap header sits at -8) pub(super) fn emit_construct(emitter: &mut Emitter) { - let initial_frame_bytes = fiber_initial_stack_frame_bytes(emitter.target.arch); - let initial_entry_offset = fiber_initial_entry_offset(emitter.target.arch); + let initial_frame_bytes = fiber_initial_stack_frame_bytes(emitter.target); + let initial_entry_offset = fiber_initial_entry_offset(emitter.target); emitter.blank(); emitter.comment("--- runtime: fiber_construct ---"); diff --git a/src/codegen_support/runtime/fibers/api/x86_64.rs b/src/codegen_support/runtime/fibers/api/x86_64.rs index 8d6cf34e81..bd05cf9bac 100644 --- a/src/codegen_support/runtime/fibers/api/x86_64.rs +++ b/src/codegen_support/runtime/fibers/api/x86_64.rs @@ -56,8 +56,8 @@ pub(super) fn emit_throw_state_error_x86_64(emitter: &mut Emitter) { /// Returns a pointer to the freshly built Fiber object in rax. /// On stack allocation failure branches to `__rt_fiber_throw_state_error` (no return). pub(super) fn emit_construct_x86_64(emitter: &mut Emitter) { - let initial_frame_bytes = fiber_initial_stack_frame_bytes(emitter.target.arch); - let initial_entry_offset = fiber_initial_entry_offset(emitter.target.arch); + let initial_frame_bytes = fiber_initial_stack_frame_bytes(emitter.target); + let initial_entry_offset = fiber_initial_entry_offset(emitter.target); emitter.blank(); emitter.comment("--- runtime: fiber_construct ---"); diff --git a/src/codegen_support/runtime/fibers/entry.rs b/src/codegen_support/runtime/fibers/entry.rs index 44c1793f29..40773b4c55 100644 --- a/src/codegen_support/runtime/fibers/entry.rs +++ b/src/codegen_support/runtime/fibers/entry.rs @@ -144,10 +144,10 @@ fn emit_x86_64(emitter: &mut Emitter) { // -- establish a tiny frame on this fiber's fresh stack -- emitter.instruction("push rbp"); // preserve a zero-equivalent caller frame pointer slot for walkers emitter.instruction("mov rbp, rsp"); // anchor the frame pointer at the new bottom of the fiber stack - emitter.instruction("sub rsp, 8"); // align the fresh stack for SysV calls after the synthetic entry jump + emitter.instruction(&format!("sub rsp, {}", TRY_HANDLER_SLOT_SIZE + 8)); // reserve the boundary handler and align calls in one fixed unwindable frame + emitter.instruction("sub rbp, 8"); // keep rbp at the PE-encodable rsp+224 frame offset // -- install a sentinel exception handler for exceptions escaping the callback -- - emitter.instruction(&format!("sub rsp, {}", TRY_HANDLER_SLOT_SIZE)); // reserve TRY_HANDLER_SLOT_SIZE bytes for the boundary handler abi::emit_load_symbol_to_reg(emitter, "r10", "_exc_handler_top", 0); // r10 = previous head of the handler chain emitter.instruction("mov QWORD PTR [rsp], r10"); // handler.next = previous chain head emitter.instruction("mov QWORD PTR [rsp + 8], 0"); // handler.activation_record = NULL @@ -179,7 +179,7 @@ fn emit_x86_64(emitter: &mut Emitter) { // -- call through the generated Fiber wrapper -- emitter.label("__rt_fiber_entry_call_wrapper"); emitter.instruction("mov rdi, r12"); // pass Fiber* to the wrapper so it can load args and captures - emitter.instruction("call r13"); // call wrapper; rax returns a boxed Mixed terminal value + emitter.emit_platform_callback_call("r13", 1); // -- store the return value into transfer_value and mark Terminated -- abi::emit_load_symbol_to_reg(emitter, "r12", "_fiber_current", 0); // reload r12 because the callback may have clobbered caller-saved registers diff --git a/src/codegen_support/runtime/fibers/switch.rs b/src/codegen_support/runtime/fibers/switch.rs index 3c10a98af1..e88ff8d223 100644 --- a/src/codegen_support/runtime/fibers/switch.rs +++ b/src/codegen_support/runtime/fibers/switch.rs @@ -18,24 +18,54 @@ //! * General-purpose: rbx, rbp, r12–r15 //! * Resume address: normal call return address, left above the saved GPRs //! Total = 6 GPRs + 1 return address = 56 bytes for a fresh stack frame. +//! +//! Windows x86_64 state preserved across switches: +//! * General-purpose: rbx, rbp, rsi, rdi, r12–r15 +//! * Floating-point: xmm6–xmm15 +//! * Resume address: normal call return address above the aligned save area +//! Total = 8 GPRs + 168-byte aligned XMM area + 1 return address = 240 bytes. use crate::codegen_support::abi; use crate::codegen_support::emit::Emitter; -use crate::codegen_support::platform::Arch; +use crate::codegen_support::platform::{Arch, Platform, Target}; -use super::{FIBER_OWN_CALL_FRAME_OFFSET, FIBER_OWN_EXC_HEAD_OFFSET, FIBER_SAVED_SP_OFFSET}; +use super::{ + alloc::FIBER_GUARD_PAGE_SIZE, + FIBER_OWN_CALL_FRAME_OFFSET, FIBER_OWN_EXC_HEAD_OFFSET, FIBER_SAVED_SP_OFFSET, + FIBER_STACK_BASE_OFFSET, FIBER_STACK_TOP_OFFSET, +}; /// Total bytes saved on the stack by an AArch64 context switch (must stay 16-aligned). const AARCH64_SWITCH_SAVE_BYTES: i32 = 160; /// Total bytes pushed by a Linux x86_64 context switch, excluding the call return address. -const X86_64_SWITCH_SAVE_BYTES: i32 = 48; +const LINUX_X86_64_SWITCH_SAVE_BYTES: i32 = 48; /// Total bytes present in a fresh Linux x86_64 fiber frame, including the resume address. -const X86_64_INITIAL_FRAME_BYTES: i32 = X86_64_SWITCH_SAVE_BYTES + 8; +const LINUX_X86_64_INITIAL_FRAME_BYTES: i32 = LINUX_X86_64_SWITCH_SAVE_BYTES + 8; /// Offset within the Linux x86_64 initial frame where the resume address lives. -const X86_64_INITIAL_FRAME_RIP_OFFSET: i32 = X86_64_SWITCH_SAVE_BYTES; +const LINUX_X86_64_INITIAL_FRAME_RIP_OFFSET: i32 = LINUX_X86_64_SWITCH_SAVE_BYTES; + +/// Total bytes pushed for the eight MS x64 callee-saved general-purpose registers. +const WINDOWS_X86_64_GPR_SAVE_BYTES: i32 = 64; + +/// Total bytes occupied by the ten MS x64 callee-saved XMM registers. +const WINDOWS_X86_64_XMM_SAVE_BYTES: i32 = 160; + +/// Stack reservation for the Windows XMM save area, including the alignment pad. +const WINDOWS_X86_64_XMM_STACK_RESERVE_BYTES: i32 = WINDOWS_X86_64_XMM_SAVE_BYTES + 8; + +/// Total bytes present in a fresh Windows x64 fiber frame, including the resume address. +const WINDOWS_X86_64_INITIAL_FRAME_BYTES: i32 = + WINDOWS_X86_64_GPR_SAVE_BYTES + WINDOWS_X86_64_XMM_STACK_RESERVE_BYTES + 8; + +/// Offset within the Windows x64 initial frame where the resume address lives. +const WINDOWS_X86_64_INITIAL_FRAME_RIP_OFFSET: i32 = + WINDOWS_X86_64_GPR_SAVE_BYTES + WINDOWS_X86_64_XMM_STACK_RESERVE_BYTES; + +/// Windows x64 TEB offset of the allocation base for the active stack. +const WINDOWS_X64_TEB_DEALLOCATION_STACK_OFFSET: i32 = 0x1478; /// Emits `__rt_fiber_switch`: a cooperative context switch between Fiber execution contexts. /// @@ -61,6 +91,11 @@ const X86_64_INITIAL_FRAME_RIP_OFFSET: i32 = X86_64_SWITCH_SAVE_BYTES; /// - ARM64: saves x19–x28, x29, x30, d8–d15 (160 bytes, 16-aligned) to the source stack, then restores the /// same register set from the target's stack before returning. /// - x86_64: uses the matched helper `emit_x86_64` which saves/restores rbx, rbp, r12–r15 per the SysV ABI. +/// On the Windows x86_64 target, `emit_x86_64` additionally resyncs the TEB stack metadata +/// (`StackBase`, `StackLimit`, and `DeallocationStack`) to whichever stack is about to run, +/// snapshotting/restoring the main thread's values through `_fiber_main_saved_*`; other targets +/// are unaffected. `NT_TIB::FiberData` deliberately remains owned by Windows because elephc's +/// manual stacks are not Win32 `CreateFiber` objects. /// /// Called from `emit_fiber_switch` on ARM64 targets. pub fn emit_fiber_switch(emitter: &mut Emitter) { @@ -149,14 +184,16 @@ pub fn emit_fiber_switch(emitter: &mut Emitter) { /// Returns the total bytes reserved on a freshly-created fiber stack for the entry frame. /// /// ARM64: equal to `AARCH64_SWITCH_SAVE_BYTES` (160 bytes, 16-aligned). -/// x86_64: equal to `X86_64_INITIAL_FRAME_BYTES` (56 bytes, including the resume address slot). +/// Linux x86_64: 56 bytes (six SysV callee-saved GPRs plus the resume address). +/// Windows x86_64: 240 bytes (eight MS x64 GPRs, ten 16-byte XMM values, alignment, and RIP). /// /// Used during fiber creation to allocate the initial stack area so the first switch into the fiber /// can restore registers without reading uninitialized memory. -pub(crate) fn fiber_initial_stack_frame_bytes(arch: Arch) -> i32 { - match arch { - Arch::AArch64 => AARCH64_SWITCH_SAVE_BYTES, - Arch::X86_64 => X86_64_INITIAL_FRAME_BYTES, +pub(crate) fn fiber_initial_stack_frame_bytes(target: Target) -> i32 { + match (target.platform, target.arch) { + (_, Arch::AArch64) => AARCH64_SWITCH_SAVE_BYTES, + (Platform::Windows, Arch::X86_64) => WINDOWS_X86_64_INITIAL_FRAME_BYTES, + (_, Arch::X86_64) => LINUX_X86_64_INITIAL_FRAME_BYTES, } } @@ -164,29 +201,41 @@ pub(crate) fn fiber_initial_stack_frame_bytes(arch: Arch) -> i32 { /// /// ARM64: offset 88 — the entry trampoline lives 88 bytes below the frame base (within the 160-byte save area, /// at the slot previously used by the saved x30/LR, which is the resume address for a new fiber). -/// x86_64: `X86_64_INITIAL_FRAME_RIP_OFFSET` — the resume address occupies the 8-byte slot immediately -/// after the saved GPRs, at offset 48 in a 56-byte initial frame. +/// Linux x86_64: the resume address follows six saved GPRs at offset 48. +/// Windows x86_64: it follows eight saved GPRs and the aligned XMM6–XMM15 area at offset 232. /// /// Used when creating a fiber to write the entry-point address into the correct slot so the first switch /// to that fiber jumps to the fiber's trampoline. -pub(crate) fn fiber_initial_entry_offset(arch: Arch) -> i32 { - match arch { - Arch::AArch64 => 88, - Arch::X86_64 => X86_64_INITIAL_FRAME_RIP_OFFSET, +pub(crate) fn fiber_initial_entry_offset(target: Target) -> i32 { + match (target.platform, target.arch) { + (_, Arch::AArch64) => 88, + (Platform::Windows, Arch::X86_64) => WINDOWS_X86_64_INITIAL_FRAME_RIP_OFFSET, + (_, Arch::X86_64) => LINUX_X86_64_INITIAL_FRAME_RIP_OFFSET, } } /// Emits the x86_64 SysV ABI variant of `__rt_fiber_switch`. /// -/// Saves the source context's callee-saved registers (rbx, rbp, r12–r15) and resume address to its own stack, +/// Saves the source context's callee-saved registers and resume address to its own stack, /// persists SP and exception/call-frame chain heads into either the source Fiber object or the main-thread /// globals, then restores the target context's state and returns into it. /// /// # Differences from ARM64 -/// - Saves 6 GPRs (48 bytes) + 1 resume address (8 bytes) = 56-byte frame, excluding the call-return address -/// that sits above the frame (handled by `ret`). +/// - Linux saves 6 SysV GPRs (48 bytes) + 1 resume address (8 bytes) = a 56-byte frame. +/// - Windows additionally saves rsi/rdi and xmm6–xmm15 in a 16-byte-aligned area: 240 bytes total. /// - Uses a `push`/`pop` sequence rather than a contiguous store; the return address is implicit in the `ret`. -/// - Entry trampoline address is stored at `X86_64_INITIAL_FRAME_RIP_OFFSET` (48) in the initial frame. +/// - The entry trampoline offset is target-aware so the fresh frame matches the restore order. +/// +/// # Windows (PE32+) TEB resync +/// - On the Windows x86_64 target only, the switch additionally resyncs `NT_TIB::StackBase` +/// (`gs:[0x08]`), `NT_TIB::StackLimit` (`gs:[0x10]`), and `TEB::DeallocationStack` +/// (`gs:[0x1478]`) to whichever stack is about to run. The main thread's values are snapshotted +/// into `_fiber_main_saved_*` globals when leaving it and restored when switching back. +/// - `NT_TIB::FiberData` (`gs:[0x20]`) is intentionally not changed. elephc switches private +/// runtime stacks manually and does not construct the opaque object expected by the Win32 Fiber +/// and FLS APIs. Keeping the OS value untouched preserves an enclosing Win32 fiber identity and +/// avoids falsely advertising an elephc `Fiber*` as a Win32 fiber control block. +/// - Non-Windows targets are unaffected. /// /// Called from `emit_fiber_switch` when `emitter.target.arch == Arch::X86_64`. fn emit_x86_64(emitter: &mut Emitter) { @@ -202,6 +251,22 @@ fn emit_x86_64(emitter: &mut Emitter) { emitter.instruction("push r14"); // preserve the third source context callee-saved scratch register emitter.instruction("push r15"); // preserve the fourth source context callee-saved scratch register + if emitter.target.platform == Platform::Windows { + emitter.instruction("push rsi"); // preserve MS x64's fifth callee-saved general-purpose register + emitter.instruction("push rdi"); // preserve MS x64's sixth callee-saved general-purpose register + emitter.instruction("sub rsp, 168"); // reserve aligned XMM6–XMM15 state plus the 8-byte alignment pad + emitter.instruction("movaps XMMWORD PTR [rsp], xmm6"); // preserve MS x64 callee-saved XMM6 in its aligned context slot + emitter.instruction("movaps XMMWORD PTR [rsp + 16], xmm7"); // preserve MS x64 callee-saved XMM7 in its aligned context slot + emitter.instruction("movaps XMMWORD PTR [rsp + 32], xmm8"); // preserve MS x64 callee-saved XMM8 in its aligned context slot + emitter.instruction("movaps XMMWORD PTR [rsp + 48], xmm9"); // preserve MS x64 callee-saved XMM9 in its aligned context slot + emitter.instruction("movaps XMMWORD PTR [rsp + 64], xmm10"); // preserve MS x64 callee-saved XMM10 in its aligned context slot + emitter.instruction("movaps XMMWORD PTR [rsp + 80], xmm11"); // preserve MS x64 callee-saved XMM11 in its aligned context slot + emitter.instruction("movaps XMMWORD PTR [rsp + 96], xmm12"); // preserve MS x64 callee-saved XMM12 in its aligned context slot + emitter.instruction("movaps XMMWORD PTR [rsp + 112], xmm13"); // preserve MS x64 callee-saved XMM13 in its aligned context slot + emitter.instruction("movaps XMMWORD PTR [rsp + 128], xmm14"); // preserve MS x64 callee-saved XMM14 in its aligned context slot + emitter.instruction("movaps XMMWORD PTR [rsp + 144], xmm15"); // preserve MS x64 callee-saved XMM15 in its aligned context slot + } + // -- determine the source context (current fiber, or main if NULL) -- abi::emit_load_symbol_to_reg(emitter, "r10", "_fiber_current", 0); // r10 = source fiber* (NULL means we're suspending the main thread) emitter.instruction("mov r11, rsp"); // r11 = SP to remember as the source context's resume point @@ -224,6 +289,26 @@ fn emit_x86_64(emitter: &mut Emitter) { abi::emit_load_symbol_to_reg(emitter, "r11", "_exc_call_frame_top", 0); // r11 = current head of the main-thread cleanup chain abi::emit_store_reg_to_symbol(emitter, "r11", "_fiber_main_saved_call_frame", 0); // _fiber_main_saved_call_frame = main thread cleanup chain head + // -- Windows: snapshot the main thread's TEB stack metadata so a later switch + // back to main can restore it. FiberData stays untouched because this is a + // manual stack switch, not a Win32 CreateFiber/SwitchToFiber transition. -- + if emitter.target.platform == Platform::Windows { + emitter.instruction("mov r11, QWORD PTR gs:[8]"); // r11 = TEB StackBase (main stack high address) + abi::emit_store_reg_to_symbol(emitter, "r11", "_fiber_main_saved_stack_base", 0); // remember main's StackBase across the fiber run + emitter.instruction("mov r11, QWORD PTR gs:[16]"); // r11 = TEB StackLimit (main stack low address) + abi::emit_store_reg_to_symbol(emitter, "r11", "_fiber_main_saved_stack_limit", 0); // remember main's StackLimit across the fiber run + emitter.instruction(&format!( // load the main stack reservation base from the TEB + "mov r11, QWORD PTR gs:[{}]", + WINDOWS_X64_TEB_DEALLOCATION_STACK_OFFSET + )); + abi::emit_store_reg_to_symbol( + emitter, + "r11", + "_fiber_main_saved_deallocation_stack", + 0, + ); // remember main's stack allocation base across the fiber run + } + // -- swap _fiber_current to the target and load its context -- emitter.label("__rt_fiber_switch_load_target"); abi::emit_store_reg_to_symbol(emitter, "rdi", "_fiber_current", 0); // _fiber_current = target fiber* (or NULL = main) @@ -235,6 +320,21 @@ fn emit_x86_64(emitter: &mut Emitter) { abi::emit_store_reg_to_symbol(emitter, "r11", "_exc_handler_top", 0); // restore the target fiber's handler chain head globally emitter.instruction(&format!("mov r11, QWORD PTR [rdi + {}]", FIBER_OWN_CALL_FRAME_OFFSET)); // r11 = target fiber's cleanup chain head abi::emit_store_reg_to_symbol(emitter, "r11", "_exc_call_frame_top", 0); // restore the target fiber's cleanup chain head globally + + // -- Windows: point all TEB stack metadata at the target fiber. StackLimit is + // the first usable byte above the guard; DeallocationStack is the mapping base. -- + if emitter.target.platform == Platform::Windows { + emitter.instruction(&format!("mov r11, QWORD PTR [rdi + {}]", FIBER_STACK_TOP_OFFSET)); // r11 = fiber stack_top (high address) + emitter.instruction("mov QWORD PTR gs:[8], r11"); // TEB StackBase = fiber stack_top + emitter.instruction(&format!("mov r11, QWORD PTR [rdi + {}]", FIBER_STACK_BASE_OFFSET)); // r11 = fiber stack_base (low address) + emitter.instruction(&format!("add r11, {}", FIBER_GUARD_PAGE_SIZE)); // advance past the protected guard region + emitter.instruction("mov QWORD PTR gs:[16], r11"); // TEB StackLimit = first usable stack byte + emitter.instruction(&format!("mov r11, QWORD PTR [rdi + {}]", FIBER_STACK_BASE_OFFSET)); // reload the fiber mapping base + emitter.instruction(&format!( // store the fiber mapping base in the TEB deallocation slot + "mov QWORD PTR gs:[{}], r11", + WINDOWS_X64_TEB_DEALLOCATION_STACK_OFFSET + )); + } emitter.instruction(&format!("mov rsp, QWORD PTR [rdi + {}]", FIBER_SAVED_SP_OFFSET)); // adopt the target fiber's saved stack pointer emitter.instruction("jmp __rt_fiber_switch_restore"); // proceed to restore callee-saved registers @@ -244,10 +344,43 @@ fn emit_x86_64(emitter: &mut Emitter) { abi::emit_store_reg_to_symbol(emitter, "r11", "_exc_handler_top", 0); // restore the main thread handler chain head globally abi::emit_load_symbol_to_reg(emitter, "r11", "_fiber_main_saved_call_frame", 0); // r11 = main thread's saved cleanup chain head abi::emit_store_reg_to_symbol(emitter, "r11", "_exc_call_frame_top", 0); // restore the main thread cleanup chain head globally + + // -- Windows: restore the main thread's complete saved TEB stack metadata. -- + if emitter.target.platform == Platform::Windows { + abi::emit_load_symbol_to_reg(emitter, "r11", "_fiber_main_saved_stack_base", 0); // r11 = main's saved StackBase + emitter.instruction("mov QWORD PTR gs:[8], r11"); // TEB StackBase = main stack high address + abi::emit_load_symbol_to_reg(emitter, "r11", "_fiber_main_saved_stack_limit", 0); // r11 = main's saved StackLimit + emitter.instruction("mov QWORD PTR gs:[16], r11"); // TEB StackLimit = main stack low address + abi::emit_load_symbol_to_reg( + emitter, + "r11", + "_fiber_main_saved_deallocation_stack", + 0, + ); // r11 = main's saved stack allocation base + emitter.instruction(&format!( // restore the main stack reservation base in the TEB + "mov QWORD PTR gs:[{}], r11", + WINDOWS_X64_TEB_DEALLOCATION_STACK_OFFSET + )); + } abi::emit_load_symbol_to_reg(emitter, "rsp", "_fiber_main_saved_sp", 0); // adopt the main thread's saved stack pointer // -- restore callee-saved state from the target stack and return into it -- emitter.label("__rt_fiber_switch_restore"); + if emitter.target.platform == Platform::Windows { + emitter.instruction("movaps xmm6, XMMWORD PTR [rsp]"); // restore MS x64 callee-saved XMM6 from the aligned context slot + emitter.instruction("movaps xmm7, XMMWORD PTR [rsp + 16]"); // restore MS x64 callee-saved XMM7 from the aligned context slot + emitter.instruction("movaps xmm8, XMMWORD PTR [rsp + 32]"); // restore MS x64 callee-saved XMM8 from the aligned context slot + emitter.instruction("movaps xmm9, XMMWORD PTR [rsp + 48]"); // restore MS x64 callee-saved XMM9 from the aligned context slot + emitter.instruction("movaps xmm10, XMMWORD PTR [rsp + 64]"); // restore MS x64 callee-saved XMM10 from the aligned context slot + emitter.instruction("movaps xmm11, XMMWORD PTR [rsp + 80]"); // restore MS x64 callee-saved XMM11 from the aligned context slot + emitter.instruction("movaps xmm12, XMMWORD PTR [rsp + 96]"); // restore MS x64 callee-saved XMM12 from the aligned context slot + emitter.instruction("movaps xmm13, XMMWORD PTR [rsp + 112]"); // restore MS x64 callee-saved XMM13 from the aligned context slot + emitter.instruction("movaps xmm14, XMMWORD PTR [rsp + 128]"); // restore MS x64 callee-saved XMM14 from the aligned context slot + emitter.instruction("movaps xmm15, XMMWORD PTR [rsp + 144]"); // restore MS x64 callee-saved XMM15 from the aligned context slot + emitter.instruction("add rsp, 168"); // release the aligned XMM save area and alignment pad + emitter.instruction("pop rdi"); // restore MS x64 callee-saved rdi + emitter.instruction("pop rsi"); // restore MS x64 callee-saved rsi + } emitter.instruction("pop r15"); // restore the fourth target callee-saved scratch register emitter.instruction("pop r14"); // restore the third target callee-saved scratch register emitter.instruction("pop r13"); // restore the second target callee-saved scratch register @@ -256,3 +389,111 @@ fn emit_x86_64(emitter: &mut Emitter) { emitter.instruction("pop rbx"); // restore the target callee-saved base register emitter.instruction("ret"); // resume the target context using its saved return address } + +#[cfg(test)] +mod tests { + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::*; + + /// Verifies that the Windows x86_64 fiber switch resyncs every TEB stack field, + /// keeps the guard page below StackLimit, and leaves Win32 FiberData untouched. + #[test] + fn test_x86_64_windows_switch_resyncs_complete_teb_stack_metadata() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_fiber_switch(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("mov QWORD PTR gs:[8], r11")); + assert!(asm.contains("mov QWORD PTR gs:[16], r11")); + assert!(asm.contains("mov QWORD PTR gs:[5240], r11")); + assert!(asm.contains("_fiber_main_saved_stack_base")); + assert!(asm.contains("_fiber_main_saved_stack_limit")); + assert!(asm.contains("_fiber_main_saved_deallocation_stack")); + assert!(asm.contains("mov r11, QWORD PTR gs:[8]")); + assert!(asm.contains("mov r11, QWORD PTR gs:[5240]")); + assert!(asm.contains("add r11, 16384")); + assert!(!asm.contains("gs:[32]")); + } + + /// Verifies the Windows switch saves and restores every MS x64 nonvolatile register. + #[test] + fn test_x86_64_windows_switch_preserves_full_ms_x64_context() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_fiber_switch(&mut emitter); + let asm = emitter.output(); + + let save_area = asm.find("sub rsp, 168").expect("missing aligned XMM save area"); + let restore_area = asm.find("add rsp, 168").expect("missing aligned XMM restore area"); + assert!(save_area < asm.find("movaps XMMWORD PTR [rsp], xmm6").unwrap()); + assert!(restore_area > asm.find("movaps xmm15, XMMWORD PTR [rsp + 144]").unwrap()); + assert!(asm.contains("push rsi")); + assert!(asm.contains("push rdi")); + assert!(asm.contains("pop rdi")); + assert!(asm.contains("pop rsi")); + + for (offset, register) in [ + (0, "xmm6"), + (16, "xmm7"), + (32, "xmm8"), + (48, "xmm9"), + (64, "xmm10"), + (80, "xmm11"), + (96, "xmm12"), + (112, "xmm13"), + (128, "xmm14"), + (144, "xmm15"), + ] { + let slot = if offset == 0 { + "[rsp]".to_owned() + } else { + format!("[rsp + {offset}]") + }; + assert!(asm.contains(&format!("movaps XMMWORD PTR {slot}, {register}"))); + assert!(asm.contains(&format!("movaps {register}, XMMWORD PTR {slot}"))); + } + } + + /// Verifies each target's fresh fiber frame matches its context-switch restore sequence. + #[test] + fn test_fiber_initial_frame_layout_is_target_aware() { + let windows = Target::new(Platform::Windows, Arch::X86_64); + let linux = Target::new(Platform::Linux, Arch::X86_64); + let arm64 = Target::new(Platform::MacOS, Arch::AArch64); + + assert_eq!(fiber_initial_stack_frame_bytes(windows), 240); + assert_eq!(fiber_initial_entry_offset(windows), 232); + assert_eq!(WINDOWS_X86_64_XMM_STACK_RESERVE_BYTES % 16, 8); + assert_eq!(WINDOWS_X86_64_INITIAL_FRAME_BYTES % 16, 0); + assert_eq!(WINDOWS_X86_64_INITIAL_FRAME_RIP_OFFSET % 16, 8); + assert_eq!(fiber_initial_stack_frame_bytes(linux), 56); + assert_eq!(fiber_initial_entry_offset(linux), 48); + assert_eq!(fiber_initial_stack_frame_bytes(arm64), 160); + assert_eq!(fiber_initial_entry_offset(arm64), 88); + } + + /// Verifies that the Linux x86_64 fiber switch is byte-identical to the SysV-only + /// switch: no `gs:` segment override and no TEB resync symbols anywhere in the output. + #[test] + fn test_x86_64_linux_switch_has_no_teb_resync() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_fiber_switch(&mut emitter); + let asm = emitter.output(); + assert!(!asm.contains("gs:")); + assert!(!asm.contains("_fiber_main_saved_stack")); + assert!(!asm.contains("push rsi")); + assert!(!asm.contains("push rdi")); + assert!(!asm.contains("movaps XMMWORD PTR")); + assert!(!asm.contains("sub rsp, 168")); + } + + /// Verifies that the AArch64 fiber switch is unaffected by the Windows TIB resync: + /// no `gs:` segment override and no TEB resync symbols anywhere in the output. + #[test] + fn test_aarch64_switch_has_no_teb_resync() { + let mut emitter = Emitter::new(Target::new(Platform::MacOS, Arch::AArch64)); + emit_fiber_switch(&mut emitter); + let asm = emitter.output(); + assert!(!asm.contains("gs:")); + assert!(!asm.contains("_fiber_main_saved_stack")); + } +} diff --git a/src/codegen_support/runtime/io/basename.rs b/src/codegen_support/runtime/io/basename.rs index 77542a7da5..1f54dcf8c3 100644 --- a/src/codegen_support/runtime/io/basename.rs +++ b/src/codegen_support/runtime/io/basename.rs @@ -8,7 +8,7 @@ //! Key details: //! - I/O helpers bridge PHP strings, resources, descriptors, and libc calls while returning runtime arrays or pointer/length strings. -use crate::codegen_support::{emit::Emitter, platform::Arch}; +use crate::codegen_support::{emit::Emitter, platform::Arch, platform::Platform}; /// Emits the `__rt_basename` runtime helper for ARM64 targets, with an x86_64 Linux variant. /// @@ -115,7 +115,16 @@ fn emit_basename_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("sub r8, 1"); // r8 = index of the last byte emitter.instruction("movzx r9d, BYTE PTR [rax + r8]"); // load the last byte emitter.instruction("cmp r9b, 0x2F"); // is it a forward slash? - emitter.instruction("jne __rt_basename_scan_init_x86"); // not a slash: scan for the last separator + if emitter.platform == Platform::Windows { + // PHP's IS_SLASH accepts both separators on Windows (Zend/zend_virtual_cwd.h), + // so a backslash-terminated path must be trimmed exactly like a slash one. + emitter.instruction("je __rt_basename_strip_sep_x86"); // a forward slash is already a separator + emitter.instruction("cmp r9b, 0x5C"); // is it a backslash separator? + emitter.instruction("jne __rt_basename_scan_init_x86"); // neither separator: scan for the last one + emitter.label("__rt_basename_strip_sep_x86"); + } else { + emitter.instruction("jne __rt_basename_scan_init_x86"); // not a slash: scan for the last separator + } emitter.instruction("sub rdx, 1"); // drop the trailing slash from the slice length emitter.instruction("jmp __rt_basename_strip_x86"); // continue stripping further trailing slashes @@ -130,6 +139,10 @@ fn emit_basename_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("movzx r10d, BYTE PTR [rax + r9]"); // load the candidate byte emitter.instruction("cmp r10b, 0x2F"); // is the candidate a separator? emitter.instruction("je __rt_basename_slash_x86"); // found the rightmost slash, slice past it + if emitter.platform == Platform::Windows { + emitter.instruction("cmp r10b, 0x5C"); // is the candidate a backslash separator? + emitter.instruction("je __rt_basename_slash_x86"); // found the rightmost separator, slice past it + } emitter.instruction("sub r8, 1"); // step left toward the start of the path emitter.instruction("jmp __rt_basename_scan_x86"); // continue the right-to-left scan diff --git a/src/codegen_support/runtime/io/closedir.rs b/src/codegen_support/runtime/io/closedir.rs index f099ad8b9f..a54ddcc351 100644 --- a/src/codegen_support/runtime/io/closedir.rs +++ b/src/codegen_support/runtime/io/closedir.rs @@ -9,6 +9,10 @@ //! - The `DIR*` recorded by `__rt_opendir` in `_dir_handles` is cleared and //! handed back to libc `closedir`, which closes the stream and its descriptor. //! - A descriptor with no recorded `DIR*` is a no-op. +//! - The x86_64 libc-`DIR*` `closedir` call site routes through +//! `Emitter::emit_call_c`. On Windows, `__rt_sys_closedir` calls `FindClose` +//! and releases the retained search-pattern and directory-state allocations. +//! The glob-fd branch continues through its separate `globfree`/`close` path. use crate::codegen_support::{abi, emit::Emitter, platform::Arch}; @@ -86,7 +90,7 @@ fn emit_closedir_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jz __rt_closedir_done_x86"); // nothing recorded: nothing to close emitter.instruction("mov QWORD PTR [r9 + rdi * 8], 0"); // clear the fd->DIR* table entry emitter.instruction("mov rdi, r10"); // DIR* argument for closedir - emitter.bl_c("closedir"); + emitter.emit_call_c("closedir"); // Windows: close the search handle and release the DIR state emitter.label("__rt_closedir_done_x86"); emitter.instruction("add rsp, 16"); // release the scratch slot diff --git a/src/codegen_support/runtime/io/dirname.rs b/src/codegen_support/runtime/io/dirname.rs index d8fd22d3f9..14b8196457 100644 --- a/src/codegen_support/runtime/io/dirname.rs +++ b/src/codegen_support/runtime/io/dirname.rs @@ -8,7 +8,7 @@ //! Key details: //! - I/O helpers bridge PHP strings, resources, descriptors, and libc calls while returning runtime arrays or pointer/length strings. -use crate::codegen_support::{emit::Emitter, platform::Arch}; +use crate::codegen_support::{emit::Emitter, platform::Arch, platform::Platform}; /// Emits the `__rt_dirname` runtime helper for the current target. /// @@ -30,6 +30,10 @@ use crate::codegen_support::{emit::Emitter, platform::Arch}; /// - trailing slashes are stripped before locating the final separator /// - result drops the trailing slash unless the parent is the filesystem root pub fn emit_dirname(emitter: &mut Emitter) { + if emitter.platform == Platform::Windows { + emit_dirname_windows_x86_64(emitter); + return; + } if emitter.target.arch == Arch::X86_64 { emit_dirname_linux_x86_64(emitter); return; @@ -180,3 +184,126 @@ fn emit_dirname_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_dirname_done_x86"); emitter.instruction("ret"); // return parent-dir slice in rax/rdx } + +/// Emits the Windows x86_64 implementation of `__rt_dirname`. +/// +/// Windows is an x86_64-only target, so this replaces the POSIX emitter outright +/// rather than threading platform tests through it. It follows php-src +/// `php_win32_ioutil_dirname` (win32/ioutil.c): +/// +/// 1. an `:` drive prefix is skipped and never scanned, so the result keeps +/// it; a bare `"C:"` is returned unchanged; +/// 2. both `/` and `\` are separators (`PHP_WIN32_IOUTIL_IS_SLASHW`, mirroring +/// `IS_SLASH` in Zend/zend_virtual_cwd.h); +/// 3. trailing separators are stripped, then the filename, then the separators +/// before it; +/// 4. running out of bytes yields the root, and finding no separator yields `"."`. +/// +/// # ABI +/// - Input: `rax` = path pointer, `rdx` = path length +/// - Output: `rax` = parent pointer (a slice of the input), `rdx` = parent length +/// +/// # Known divergence +/// php-src rewrites the root case to `PHP_WIN32_IOUTIL_DEFAULT_SLASH`, so it +/// normalises `dirname("C:/x")` to `"C:\"`. This returns the input's own bytes, +/// `"C:/"`, because the helper is contractually allocation-free and hands back a +/// slice. The separator is preserved rather than rewritten; every other result is +/// byte-identical to PHP. +fn emit_dirname_windows_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: dirname (windows) ---"); + emitter.label_global("__rt_dirname"); + + // ABI: rax=path_ptr, rdx=path_len. Returns rax/rdx. + // r11 holds the drive-prefix length: the scan never steps below it, which is + // what keeps "C:\x" from collapsing past the drive into "C". + + emitter.instruction("test rdx, rdx"); // empty input? + emitter.instruction("jz __rt_dirname_dot_win"); // empty path → "." + emitter.instruction("xor r11d, r11d"); // assume no drive prefix + emitter.instruction("cmp rdx, 2"); // room for an ":" prefix? + emitter.instruction("jb __rt_dirname_scan_init_win"); // too short to carry a drive letter + emitter.instruction("movzx r9d, BYTE PTR [rax + 1]"); // second byte of the path + emitter.instruction("cmp r9b, 0x3A"); // is it a colon? + emitter.instruction("jne __rt_dirname_scan_init_win"); // not a drive-qualified path + emitter.instruction("movzx r9d, BYTE PTR [rax]"); // first byte of the path + emitter.instruction("or r9b, 0x20"); // fold the drive letter to lower case + emitter.instruction("cmp r9b, 0x61"); // below 'a'? + emitter.instruction("jb __rt_dirname_scan_init_win"); // not a letter, so not a drive + emitter.instruction("cmp r9b, 0x7A"); // above 'z'? + emitter.instruction("ja __rt_dirname_scan_init_win"); // not a letter, so not a drive + emitter.instruction("mov r11d, 2"); // the drive prefix is off-limits to the scan + emitter.instruction("cmp rdx, 2"); // is the path exactly "C:"? + emitter.instruction("je __rt_dirname_done_win"); // php returns the drive unchanged + + // -- strip trailing separators -- + emitter.label("__rt_dirname_scan_init_win"); + emitter.instruction("mov r8, rdx"); // r8 = exclusive end index of the live slice + emitter.label("__rt_dirname_strip_win"); + emitter.instruction("cmp r8, r11"); // consumed everything after the drive? + emitter.instruction("jbe __rt_dirname_root_win"); // only separators remained → root + emitter.instruction("mov r9, r8"); // index of the last byte + emitter.instruction("sub r9, 1"); // step into the slice + emitter.instruction("movzx r10d, BYTE PTR [rax + r9]"); // load the last byte + emitter.instruction("cmp r10b, 0x2F"); // forward-slash separator? + emitter.instruction("je __rt_dirname_strip_step_win"); // drop it + emitter.instruction("cmp r10b, 0x5C"); // backslash separator? + emitter.instruction("jne __rt_dirname_name_win"); // neither: the filename starts here + emitter.label("__rt_dirname_strip_step_win"); + emitter.instruction("mov r8, r9"); // shrink past the trailing separator + emitter.instruction("jmp __rt_dirname_strip_win"); // keep stripping + + // -- strip the filename -- + emitter.label("__rt_dirname_name_win"); + emitter.instruction("cmp r8, r11"); // reached the drive with no separator? + emitter.instruction("jbe __rt_dirname_dot_win"); // → "." + emitter.instruction("mov r9, r8"); // candidate index + emitter.instruction("sub r9, 1"); // step the candidate left + emitter.instruction("movzx r10d, BYTE PTR [rax + r9]"); // load the candidate byte + emitter.instruction("cmp r10b, 0x2F"); // forward-slash separator? + emitter.instruction("je __rt_dirname_parent_win"); // found the parent boundary + emitter.instruction("cmp r10b, 0x5C"); // backslash separator? + emitter.instruction("je __rt_dirname_parent_win"); // found the parent boundary + emitter.instruction("mov r8, r9"); // step left over the filename + emitter.instruction("jmp __rt_dirname_name_win"); // keep scanning + + // -- strip the separators that preceded the filename -- + emitter.label("__rt_dirname_parent_win"); + emitter.instruction("mov r8, r9"); // r8 = index of the separator itself + emitter.label("__rt_dirname_parent_loop_win"); + emitter.instruction("cmp r8, r11"); // nothing but separators before it? + emitter.instruction("jbe __rt_dirname_root_win"); // → root + emitter.instruction("mov r9, r8"); // index of the preceding byte + emitter.instruction("sub r9, 1"); // step into the slice + emitter.instruction("movzx r10d, BYTE PTR [rax + r9]"); // load it + emitter.instruction("cmp r10b, 0x2F"); // another forward slash? + emitter.instruction("je __rt_dirname_parent_step_win"); // collapse it + emitter.instruction("cmp r10b, 0x5C"); // another backslash? + emitter.instruction("jne __rt_dirname_finish_win"); // parent ends on a real byte + emitter.label("__rt_dirname_parent_step_win"); + emitter.instruction("mov r8, r9"); // collapse the repeated separator + emitter.instruction("jmp __rt_dirname_parent_loop_win"); // keep collapsing + + emitter.label("__rt_dirname_finish_win"); + emitter.instruction("mov rdx, r8"); // the parent is the prefix up to r8 + emitter.instruction("ret"); // return the parent slice + + // -- the parent is the root: "" plus one separator, or a bare "\" -- + emitter.label("__rt_dirname_root_win"); + emitter.instruction("test r11, r11"); // was there a drive prefix? + emitter.instruction("jz __rt_dirname_root_literal_win"); // no drive: return the separator literal + emitter.instruction("mov rdx, 3"); // keep ":" plus the separator byte + emitter.instruction("ret"); // e.g. "C:\" for "C:\file" + emitter.label("__rt_dirname_root_literal_win"); + crate::codegen_support::abi::emit_symbol_address(emitter, "rax", "_dirname_backslash"); // result = "\" + emitter.instruction("mov rdx, 1"); // length = 1 + emitter.instruction("ret"); // php normalises a rooted path to DEFAULT_SLASH + + emitter.label("__rt_dirname_dot_win"); + crate::codegen_support::abi::emit_symbol_address(emitter, "rax", "_dirname_dot"); // result = "." + emitter.instruction("mov rdx, 1"); // length = 1 + emitter.instruction("ret"); // return "." + + emitter.label("__rt_dirname_done_win"); + emitter.instruction("ret"); // return the path unchanged (bare drive) +} diff --git a/src/codegen_support/runtime/io/feof.rs b/src/codegen_support/runtime/io/feof.rs index c2aa61c68f..64d0179cdf 100644 --- a/src/codegen_support/runtime/io/feof.rs +++ b/src/codegen_support/runtime/io/feof.rs @@ -8,7 +8,7 @@ //! Key details: //! - I/O helpers bridge PHP strings, resources, descriptors, and libc calls while returning runtime arrays or pointer/length strings. -use crate::codegen_support::{emit::Emitter, platform::Arch}; +use crate::codegen_support::{emit::Emitter, platform::{Arch, Platform}}; use crate::codegen_support::abi; /// Emits the `__rt_feof` runtime helper. @@ -53,7 +53,11 @@ fn emit_feof_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("cmp rdi, r9"); // is this a synthetic user-wrapper fd? emitter.instruction("jge __rt_user_wrapper_feof"); // dispatch into the wrapper's stream_eof instead of reading the eof-flag table - abi::emit_symbol_address(emitter, "r10", "_eof_flags"); // materialize the eof-flag table base address for the queried file descriptor - emitter.instruction("movzx eax, BYTE PTR [r10 + rdi]"); // load the tracked eof flag byte for the requested file descriptor into the integer result register + if emitter.target.platform == Platform::Windows { + emitter.instruction("call __rt_win_stream_slot"); // map opaque SOCKET/CRT descriptor to a bounded stream-state slot + emitter.instruction("mov rdi, rax"); // use only the compact slot for fixed-table addressing + } + abi::emit_symbol_address(emitter, "r10", "_eof_flags"); // materialize the eof-flag table base address for the queried stream slot + emitter.instruction("movzx eax, BYTE PTR [r10 + rdi]"); // load the tracked eof flag byte without indexing by a raw Windows handle emitter.instruction("ret"); // return the eof flag to the caller using the standard x86_64 integer result register } diff --git a/src/codegen_support/runtime/io/fgets.rs b/src/codegen_support/runtime/io/fgets.rs index e1777882e8..b547dbeb99 100644 --- a/src/codegen_support/runtime/io/fgets.rs +++ b/src/codegen_support/runtime/io/fgets.rs @@ -8,7 +8,7 @@ //! Key details: //! - I/O helpers bridge PHP strings, resources, descriptors, and libc calls while returning runtime arrays or pointer/length strings. -use crate::codegen_support::{emit::Emitter, platform::Arch}; +use crate::codegen_support::{emit::Emitter, platform::{Arch, Platform}}; use crate::codegen_support::abi; /// Reads one line from a file descriptor into the concat buffer. @@ -199,6 +199,10 @@ fn emit_fgets_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jge __rt_fgets_wrapper_entry_x86"); // wrappers read via the feof-gated stream_read loop below emitter.label("__rt_fgets_loop_x86"); + if emitter.target.platform == Platform::Windows { + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // restore the opaque descriptor before resetting timeout metadata + emitter.instruction("call __rt_win_stream_clear_timed_out"); // each new one-byte read starts with timed_out=false + } abi::emit_load_symbol_to_reg(emitter, "r10", "_concat_off", 0); // reload the current concat-buffer absolute offset before reading one more byte abi::emit_symbol_address(emitter, "r11", "_concat_buf"); // rematerialize the concat-buffer base address for the current one-byte read destination emitter.instruction("lea rsi, [r11 + r10]"); // compute the address where libc read() should append the next byte @@ -273,10 +277,22 @@ fn emit_fgets_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov r10d, DWORD PTR [rax]"); // load the thread-local errno value emitter.instruction("cmp r10d, 11"); // is this EAGAIN/EWOULDBLOCK from a nonblocking fd? emitter.instruction("je __rt_fgets_done_x86"); // would-block returns the partial line without setting EOF + if emitter.target.platform == Platform::Windows { + emitter.instruction("cmp r10d, 110"); // ETIMEDOUT is retryable stream timeout, not EOF + emitter.instruction("jne __rt_fgets_eof_x86"); // only non-timeout failures set EOF + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // restore the opaque stream descriptor for timeout metadata + emitter.instruction("call __rt_win_stream_mark_timed_out"); // record timed_out without touching EOF state + emitter.instruction("jmp __rt_fgets_done_x86"); // return the partial line without EOF + } emitter.label("__rt_fgets_eof_x86"); emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the file descriptor so the eof-flag table can mark this stream as exhausted + if emitter.target.platform == Platform::Windows { + emitter.instruction("mov rdi, r10"); // pass the opaque Windows descriptor to the slot registry + emitter.instruction("call __rt_win_stream_slot"); // obtain a bounded EOF-table slot + emitter.instruction("mov r10, rax"); // table indexing uses the compact slot, never a raw SOCKET + } abi::emit_symbol_address(emitter, "r11", "_eof_flags"); // materialize the eof-flag table base address for the current stream descriptor - emitter.instruction("mov BYTE PTR [r11 + r10], 1"); // mark the current file descriptor as EOF-reached after the zero-byte or failed read + emitter.instruction("mov BYTE PTR [r11 + r10], 1"); // mark the compact stream slot as EOF-reached after the zero-byte or failed read emitter.instruction("jmp __rt_fgets_done_x86"); // return the possibly empty borrowed slice accumulated before EOF or read failure } diff --git a/src/codegen_support/runtime/io/file_get_contents_url.rs b/src/codegen_support/runtime/io/file_get_contents_url.rs index c63c456d5a..18983f47ba 100644 --- a/src/codegen_support/runtime/io/file_get_contents_url.rs +++ b/src/codegen_support/runtime/io/file_get_contents_url.rs @@ -18,6 +18,43 @@ use crate::codegen_support::{abi, emit::Emitter, platform::Arch}; +/// Reclaims the block `__rt_stream_get_contents` owned, once the response has been persisted. +/// +/// The slurp helper returns a borrowed slice of the 64 KiB concat arena while the response fits +/// in it, and an owned `__rt_heap_alloc` block when it does not. Every URL branch then calls +/// `__rt_str_persist`, which allocates independently and byte-copies, so the slurped storage is +/// dead from that point on — but no branch reclaimed it, stranding one block per call sized to +/// the doubled accumulation capacity (a 200 KB body left ~256 KiB behind on every read). +/// +/// `__rt_heap_free_safe` separates the two shapes exactly rather than heuristically: +/// `_concat_buf` and `_heap_buf` are distinct `.comm` objects, so an arena pointer can never +/// satisfy its managed-heap window test and passes through untouched. +/// +/// `source_slot` is the frame offset where the branch saved the pre-persist pointer across its +/// close sequence. The owned copy is parked on the temporary stack rather than in a frame slot +/// so this stays independent of each branch's differing frame layout. +fn emit_release_slurped_source(emitter: &mut Emitter, source_slot: u32) { + match emitter.target.arch { + Arch::AArch64 => { + emitter.instruction(&format!("ldr x0, [sp, #{}]", source_slot)); // reload the pre-persist slurp pointer + emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the persisted copy across the release + emitter.instruction("bl __rt_heap_free_safe"); // arena slices fall through the managed-heap window test + emitter.instruction("ldp x1, x2, [sp], #16"); // restore the persisted copy as the result + } + Arch::X86_64 => { + emitter.instruction("sub rsp, 16"); // reserve one temporary slot for the persisted copy + emitter.instruction("mov QWORD PTR [rsp], rax"); // preserve the persisted copy pointer + emitter.instruction("mov QWORD PTR [rsp + 8], rdx"); // preserve the persisted copy length + let reload = format!("mov rax, QWORD PTR [rbp - {}]", source_slot); + emitter.instruction(&reload); // reload the pre-persist slurp pointer + emitter.instruction("call __rt_heap_free_safe"); // arena slices fall through the managed-heap window test + emitter.instruction("mov rax, QWORD PTR [rsp]"); // restore the persisted copy pointer + emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // restore the persisted copy length + emitter.instruction("add rsp, 16"); // release the temporary slot + } + } +} + /// Emits `__rt_file_get_contents_maybe_url`. /// /// Inputs use elephc's string ABI (`x1`/`x2` on AArch64, `rax`/`rdx` on @@ -173,6 +210,7 @@ pub fn emit_file_get_contents_url(emitter: &mut Emitter) { emitter.syscall(6); // close the temporary response stream emitter.instruction("ldp x1, x2, [sp, #64]"); // restore response ptr/len emitter.instruction("bl __rt_str_persist"); // persist the response string for file_get_contents ownership + emit_release_slurped_source(emitter, 64); emitter.instruction("ldp x29, x30, [sp, #80]"); // restore frame pointer and return address emitter.instruction("add sp, sp, #96"); // release helper frame emitter.instruction("ret"); // return owned response string @@ -325,6 +363,7 @@ pub fn emit_file_get_contents_url(emitter: &mut Emitter) { emitter.syscall(6); // close the temporary response stream emitter.instruction("ldp x1, x2, [sp, #80]"); // restore response ptr/len emitter.instruction("bl __rt_str_persist"); // persist the response string for file_get_contents ownership + emit_release_slurped_source(emitter, 80); emitter.instruction("ldp x29, x30, [sp, #96]"); // restore frame pointer and return address emitter.instruction("add sp, sp, #112"); // release helper frame emitter.instruction("ret"); // return owned HTTPS response string @@ -517,20 +556,17 @@ pub fn emit_file_get_contents_url(emitter: &mut Emitter) { emitter.instruction("bl __rt_stream_get_contents"); // slurp the FTP data fd into concat buffer emitter.instruction("stp x1, x2, [sp, #64]"); // preserve response ptr/len across close emitter.instruction("ldr x0, [sp, #56]"); // reload FTP data fd - abi::emit_symbol_address(emitter, "x9", "_tls_sessions"); - emitter.instruction("ldr x10, [x9, x0, lsl #3]"); // TLS session attached to this data fd? - emitter.instruction("cbz x10, __rt_fgc_url_ftp_close_plain"); // plain FTP data fd: close directly - emitter.instruction("mov x0, x10"); // TLS handle as close helper argument + emitter.instruction("bl __rt_tls_session_clear"); // remove and return any TLS session associated with the full-width fd + emitter.instruction("cbz x0, __rt_fgc_url_ftp_close_plain"); // plain FTP data fd: close directly abi::emit_symbol_address(emitter, "x9", "_elephc_tls_close_fn"); emitter.instruction("ldr x9, [x9]"); // load elephc_tls_close entry pointer - emitter.instruction("blr x9"); // send close_notify and drop the TLS session - emitter.instruction("ldr x0, [sp, #56]"); // reload FTP data fd after TLS close - abi::emit_symbol_address(emitter, "x9", "_tls_sessions"); - emitter.instruction("str xzr, [x9, x0, lsl #3]"); // clear the TLS session slot for descriptor reuse + emitter.emit_published_bridge_call("x9"); // send close_notify through the published TLS ABI entry emitter.label("__rt_fgc_url_ftp_close_plain"); + emitter.instruction("ldr x0, [sp, #56]"); // reload the FTP data descriptor for the OS close emitter.syscall(6); // close the data connection emitter.instruction("ldp x1, x2, [sp, #64]"); // restore response ptr/len emitter.instruction("bl __rt_str_persist"); // persist the response string for file_get_contents ownership + emit_release_slurped_source(emitter, 64); emitter.instruction("ldp x29, x30, [sp, #80]"); // restore frame pointer and return address emitter.instruction("add sp, sp, #96"); // release helper frame emitter.instruction("ret"); // return owned FTP response string @@ -683,6 +719,7 @@ fn emit_file_get_contents_url_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // restore response ptr emitter.instruction("mov rdx, QWORD PTR [rbp - 80]"); // restore response length emitter.instruction("call __rt_str_persist"); // persist the response string for file_get_contents ownership + emit_release_slurped_source(emitter, 72); emitter.instruction("add rsp, 112"); // release helper locals emitter.instruction("pop rbp"); // restore caller frame pointer emitter.instruction("ret"); // return owned response string @@ -827,6 +864,7 @@ fn emit_file_get_contents_url_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rax, QWORD PTR [rbp - 88]"); // restore response ptr emitter.instruction("mov rdx, QWORD PTR [rbp - 96]"); // restore response length emitter.instruction("call __rt_str_persist"); // persist the response string for file_get_contents ownership + emit_release_slurped_source(emitter, 88); emitter.instruction("add rsp, 112"); // release helper locals emitter.instruction("pop rbp"); // restore caller frame pointer emitter.instruction("ret"); // return owned HTTPS response string @@ -995,21 +1033,19 @@ fn emit_file_get_contents_url_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // save response ptr across close emitter.instruction("mov QWORD PTR [rbp - 80], rdx"); // save response length across close emitter.instruction("mov rdi, QWORD PTR [rbp - 64]"); // reload FTP data fd - abi::emit_symbol_address(emitter, "r9", "_tls_sessions"); // TLS session handle table - emitter.instruction("mov r10, QWORD PTR [r9 + rdi * 8]"); // TLS session attached to this data fd? - emitter.instruction("test r10, r10"); // is the data fd plain? + emitter.instruction("call __rt_tls_session_clear"); // remove and return any TLS session associated with the full-width fd + emitter.instruction("test rax, rax"); // is the data fd plain? emitter.instruction("je __rt_fgc_url_ftp_close_plain_x86"); // plain FTP data fd: close directly - emitter.instruction("mov rdi, r10"); // TLS handle as close helper argument + emitter.instruction("mov rdi, rax"); // TLS handle as close helper argument abi::emit_load_symbol_to_reg(emitter, "r9", "_elephc_tls_close_fn", 0); // elephc_tls_close entry pointer - emitter.instruction("call r9"); // send close_notify and drop the TLS session - emitter.instruction("mov rdi, QWORD PTR [rbp - 64]"); // reload FTP data fd after TLS close - abi::emit_symbol_address(emitter, "r9", "_tls_sessions"); // TLS session handle table - emitter.instruction("mov QWORD PTR [r9 + rdi * 8], 0"); // clear the TLS session slot for descriptor reuse + emitter.emit_published_bridge_call("r9"); // send close_notify through the published TLS ABI entry emitter.label("__rt_fgc_url_ftp_close_plain_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 64]"); // reload the FTP data descriptor for the OS close emitter.instruction("call close"); // close the data connection emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // restore response ptr emitter.instruction("mov rdx, QWORD PTR [rbp - 80]"); // restore response length emitter.instruction("call __rt_str_persist"); // persist the response string for file_get_contents ownership + emit_release_slurped_source(emitter, 72); emitter.instruction("add rsp, 112"); // release helper locals emitter.instruction("pop rbp"); // restore caller frame pointer emitter.instruction("ret"); // return owned FTP response string @@ -1021,6 +1057,6 @@ fn emit_file_get_contents_url_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("add rsp, 112"); // release helper locals emitter.instruction("pop rbp"); // restore caller frame pointer emitter.instruction("ret"); // return failure - emitter.label("__rt_fgc_url_plain_x86"); + emitter.label_global("__rt_fgc_url_plain_x86"); emitter.instruction("jmp __rt_file_get_contents_maybe_phar"); // fallback to phar:// runtime reader or filesystem path } diff --git a/src/codegen_support/runtime/io/fopen.rs b/src/codegen_support/runtime/io/fopen.rs index 187c67d48b..06ed42a4d5 100644 --- a/src/codegen_support/runtime/io/fopen.rs +++ b/src/codegen_support/runtime/io/fopen.rs @@ -8,7 +8,7 @@ //! Key details: //! - I/O helpers bridge PHP strings, resources, descriptors, and libc calls while returning runtime arrays or pointer/length strings. -use crate::codegen_support::{abi, emit::Emitter, platform::Arch}; +use crate::codegen_support::{abi, emit::Emitter, platform::{Arch, Platform}}; /// The fixed warning text emitted when `fopen()` fails to open a file. const FOPEN_FAILED_WARNING: &str = "Warning: fopen(): Failed to open stream\n"; @@ -99,32 +99,77 @@ pub fn emit_fopen(emitter: &mut Emitter) { emitter.instruction("cmp w9, #0x72"); // compare with 'r' emitter.instruction("b.ne __rt_fopen_check_w"); // if not 'r', check for 'w' emitter.instruction("mov x1, #0"); // O_RDONLY = 0 - emitter.instruction("b __rt_fopen_check_plus"); // proceed to check for '+' modifier + emitter.instruction("b __rt_fopen_scan_modifiers"); // scan every PHP fopen modifier // -- check for 'w' mode -- emitter.label("__rt_fopen_check_w"); emitter.instruction("cmp w9, #0x77"); // compare with 'w' emitter.instruction("b.ne __rt_fopen_check_a"); // if not 'w', check for 'a' emitter.instruction(&format!("mov x1, #0x{:X}", emitter.platform.o_wronly_creat_trunc())); // O_WRONLY|O_CREAT|O_TRUNC - emitter.instruction("b __rt_fopen_check_plus"); // proceed to check for '+' modifier + emitter.instruction("b __rt_fopen_scan_modifiers"); // scan every PHP fopen modifier // -- check for 'a' mode (append) -- emitter.label("__rt_fopen_check_a"); emitter.instruction("cmp w9, #0x61"); // compare with 'a' - emitter.instruction("b.ne __rt_fopen_fail"); // reject unsupported fopen() mode letters + emitter.instruction("b.ne __rt_fopen_check_x"); // if not 'a', check for 'x' emitter.instruction(&format!("mov x1, #0x{:X}", emitter.platform.o_wronly_creat_append())); // O_WRONLY|O_CREAT|O_APPEND - // fall through to check_plus - - // -- check if second char is '+' to enable read+write -- - emitter.label("__rt_fopen_check_plus"); - emitter.instruction("cmp x4, #1"); // check if mode string has more than 1 char - emitter.instruction("b.le __rt_fopen_do_open"); // if only 1 char, skip '+' check - emitter.instruction("ldrb w10, [x3, #1]"); // load second character of mode string - emitter.instruction("cmp w10, #0x2B"); // compare with '+' - emitter.instruction("b.ne __rt_fopen_do_open"); // if not '+', keep original flags + emitter.instruction("b __rt_fopen_scan_modifiers"); // scan every PHP fopen modifier + + // -- check for 'x' mode (exclusive create) -- + emitter.label("__rt_fopen_check_x"); + emitter.instruction("cmp w9, #0x78"); // compare with 'x' + emitter.instruction("b.ne __rt_fopen_check_c"); // if not 'x', check for 'c' + emitter.instruction(&format!("mov x1, #0x{:X}", emitter.platform.o_wronly_creat() | emitter.platform.o_excl())); // O_WRONLY|O_CREAT|O_EXCL + emitter.instruction("b __rt_fopen_scan_modifiers"); // scan every PHP fopen modifier + + // -- check for 'c' mode (create without truncation) -- + emitter.label("__rt_fopen_check_c"); + emitter.instruction("cmp w9, #0x63"); // compare with 'c' + emitter.instruction("b.ne __rt_fopen_fail"); // reject an unsupported base mode + emitter.instruction(&format!("mov x1, #0x{:X}", emitter.platform.o_wronly_creat())); // O_WRONLY|O_CREAT without truncation + + // -- scan PHP fopen modifiers in every suffix position -- + emitter.label("__rt_fopen_scan_modifiers"); + emitter.instruction("mov x5, #1"); // begin after the base mode character + emitter.label("__rt_fopen_modifier_next"); + emitter.instruction("cmp x5, x4"); // consumed every mode suffix byte? + emitter.instruction("b.ge __rt_fopen_do_open"); // complete mode scan; open with accumulated flags + emitter.instruction("ldrb w6, [x3, x5]"); // load the next mode modifier + emitter.instruction("cmp w6, #0x2B"); // is this '+'? + emitter.instruction("b.ne __rt_fopen_modifier_e"); // check the next supported modifier // -- upgrade to O_RDWR: clear O_RDONLY/O_WRONLY bits, set O_RDWR -- emitter.instruction("and x1, x1, #0xFFFFFFFFFFFFFFFC"); // clear lowest 2 bits (O_RDONLY/O_WRONLY) emitter.instruction("orr x1, x1, #0x2"); // set O_RDWR flag + emitter.instruction("b __rt_fopen_modifier_advance"); // continue scanning after '+' + emitter.label("__rt_fopen_modifier_e"); + emitter.instruction("cmp w6, #0x65"); // is this close-on-exec 'e'? + if let Some(cloexec) = emitter.platform.o_cloexec() { + emitter.instruction("b.ne __rt_fopen_modifier_n"); // only supported targets add O_CLOEXEC + emitter.instruction(&format!("mov x7, #0x{:X}", cloexec)); // materialize target O_CLOEXEC + emitter.instruction("orr x1, x1, x7"); // preserve it in the open flags + emitter.instruction("b __rt_fopen_modifier_advance"); // continue scanning after 'e' + } else { + emitter.instruction("b.ne __rt_fopen_modifier_n"); // Windows accepts but cannot encode O_CLOEXEC + emitter.instruction("b __rt_fopen_modifier_advance"); // Win32 inheritance is configured separately + } + emitter.label("__rt_fopen_modifier_n"); + emitter.instruction("cmp w6, #0x6E"); // is this nonblocking 'n'? + emitter.instruction("b.ne __rt_fopen_modifier_t"); // check whether Windows text mode was requested + emitter.instruction(&format!("mov x7, #0x{:X}", emitter.platform.o_nonblock())); // materialize target O_NONBLOCK + emitter.instruction("orr x1, x1, x7"); // retain nonblocking state for the runtime + emitter.instruction("b __rt_fopen_modifier_advance"); // continue scanning after 'n' + emitter.label("__rt_fopen_modifier_t"); + emitter.instruction("cmp w6, #0x74"); // is this text-mode 't'? + if let Some(text) = emitter.platform.o_text() { + emitter.instruction("b.ne __rt_fopen_modifier_advance"); // b and other tolerated suffixes retain binary mode + emitter.instruction(&format!("mov x7, #0x{:X}", text)); // materialize Windows _O_TEXT + emitter.instruction("orr x1, x1, x7"); // let the Win32 shim configure CRT text mode + } else { + emitter.instruction("b __rt_fopen_modifier_advance"); // non-Windows targets have no CRT text-mode bit + } + emitter.label("__rt_fopen_modifier_advance"); + emitter.instruction("add x5, x5, #1"); // advance to the following suffix byte + emitter.instruction("b __rt_fopen_modifier_next"); // keep scanning the complete mode string // -- perform the open syscall -- emitter.label("__rt_fopen_do_open"); @@ -321,24 +366,69 @@ fn emit_fopen_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("cmp r11b, 0x72"); // does the mode string start with 'r' for read-only access? emitter.instruction("jne __rt_fopen_check_w_x86"); // if not, fall through to the write-mode checks emitter.instruction("xor esi, esi"); // O_RDONLY = 0 for the Linux read-only fopen() path - emitter.instruction("jmp __rt_fopen_check_plus_x86"); // continue with the optional '+' upgrade after selecting the base flags + emitter.instruction("jmp __rt_fopen_scan_modifiers_x86"); // scan every PHP fopen modifier after selecting base flags emitter.label("__rt_fopen_check_w_x86"); emitter.instruction("cmp r11b, 0x77"); // does the mode string start with 'w' for truncate-on-open writes? emitter.instruction("jne __rt_fopen_check_a_x86"); // if not, fall through to the append-mode check emitter.instruction(&format!("mov esi, 0x{:X}", emitter.platform.o_wronly_creat_trunc())); // select O_WRONLY|O_CREAT|O_TRUNC for the Linux write-mode fopen() path - emitter.instruction("jmp __rt_fopen_check_plus_x86"); // continue with the optional '+' upgrade after selecting the base flags + emitter.instruction("jmp __rt_fopen_scan_modifiers_x86"); // scan every PHP fopen modifier after selecting base flags emitter.label("__rt_fopen_check_a_x86"); emitter.instruction("cmp r11b, 0x61"); // does the mode string start with 'a' for append writes? - emitter.instruction("jne __rt_fopen_fail_x86"); // reject unsupported fopen() mode letters + emitter.instruction("jne __rt_fopen_check_x_x86"); // if not 'a', check for exclusive-create mode emitter.instruction(&format!("mov esi, 0x{:X}", emitter.platform.o_wronly_creat_append())); // select O_WRONLY|O_CREAT|O_APPEND for the Linux append-mode fopen() path - - emitter.label("__rt_fopen_check_plus_x86"); - emitter.instruction("cmp BYTE PTR [r10 + 1], 0x2B"); // does the mode string request the read-write '+' fopen() upgrade? - emitter.instruction("jne __rt_fopen_do_open_x86"); // keep the base flags when the mode string does not contain '+' + emitter.instruction("jmp __rt_fopen_scan_modifiers_x86"); // scan every PHP fopen modifier after selecting base flags + + emitter.label("__rt_fopen_check_x_x86"); + emitter.instruction("cmp r11b, 0x78"); // does the mode string start with 'x' for exclusive creation? + emitter.instruction("jne __rt_fopen_check_c_x86"); // if not, check create-without-truncation mode + emitter.instruction(&format!("mov esi, 0x{:X}", emitter.platform.o_wronly_creat() | emitter.platform.o_excl())); // select O_WRONLY|O_CREAT|O_EXCL + emitter.instruction("jmp __rt_fopen_scan_modifiers_x86"); // scan every PHP fopen modifier after selecting base flags + + emitter.label("__rt_fopen_check_c_x86"); + emitter.instruction("cmp r11b, 0x63"); // does the mode string start with 'c' without truncation? + emitter.instruction("jne __rt_fopen_fail_x86"); // reject an unsupported base fopen mode + emitter.instruction(&format!("mov esi, 0x{:X}", emitter.platform.o_wronly_creat())); // select O_WRONLY|O_CREAT without truncation + + // -- scan PHP fopen modifiers in every suffix position -- + emitter.label("__rt_fopen_scan_modifiers_x86"); + emitter.instruction("mov rcx, 1"); // begin after the base mode character + emitter.label("__rt_fopen_modifier_next_x86"); + emitter.instruction("cmp BYTE PTR [r10 + rcx], 0"); // reached the null terminator of the mode string? + emitter.instruction("je __rt_fopen_do_open_x86"); // complete mode scan; open with accumulated flags + emitter.instruction("movzx edx, BYTE PTR [r10 + rcx]"); // load the next mode modifier + emitter.instruction("cmp dl, 0x2B"); // is this '+'? + emitter.instruction("jne __rt_fopen_modifier_e_x86"); // check the next supported modifier emitter.instruction("and esi, 0xFFFFFFFC"); // clear the low access-mode bits before upgrading the Linux fopen() flags to O_RDWR emitter.instruction("or esi, 0x2"); // set O_RDWR so 'r+'/'w+'/'a+' open the file for both reading and writing + emitter.instruction("jmp __rt_fopen_modifier_advance_x86"); // continue scanning after '+' + emitter.label("__rt_fopen_modifier_e_x86"); + emitter.instruction("cmp dl, 0x65"); // is this close-on-exec 'e'? + if let Some(cloexec) = emitter.platform.o_cloexec() { + emitter.instruction("jne __rt_fopen_modifier_n_x86"); // only supported targets add O_CLOEXEC + emitter.instruction(&format!("or esi, 0x{:X}", cloexec)); // preserve target O_CLOEXEC in open flags + emitter.instruction("jmp __rt_fopen_modifier_advance_x86"); // continue scanning after 'e' + } else { + emitter.instruction("jne __rt_fopen_modifier_n_x86"); // Windows accepts but cannot encode O_CLOEXEC + emitter.instruction("jmp __rt_fopen_modifier_advance_x86"); // Win32 inheritance is configured separately + } + emitter.label("__rt_fopen_modifier_n_x86"); + emitter.instruction("cmp dl, 0x6E"); // is this nonblocking 'n'? + emitter.instruction("jne __rt_fopen_modifier_t_x86"); // check whether Windows text mode was requested + emitter.instruction(&format!("or esi, 0x{:X}", emitter.platform.o_nonblock())); // retain target O_NONBLOCK in the open flags + emitter.instruction("jmp __rt_fopen_modifier_advance_x86"); // continue scanning after 'n' + emitter.label("__rt_fopen_modifier_t_x86"); + emitter.instruction("cmp dl, 0x74"); // is this text-mode 't'? + if let Some(text) = emitter.platform.o_text() { + emitter.instruction("jne __rt_fopen_modifier_advance_x86"); // b and other tolerated suffixes retain binary mode + emitter.instruction(&format!("or esi, 0x{:X}", text)); // let the Win32 shim configure CRT text mode + } else { + emitter.instruction("jmp __rt_fopen_modifier_advance_x86"); // non-Windows targets have no CRT text-mode bit + } + emitter.label("__rt_fopen_modifier_advance_x86"); + emitter.instruction("inc rcx"); // advance to the following suffix byte + emitter.instruction("jmp __rt_fopen_modifier_next_x86"); // keep scanning the complete mode string emitter.label("__rt_fopen_do_open_x86"); emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // pass the converted C pathname as the first libc open() argument @@ -410,20 +500,43 @@ fn emit_fopen_linux_x86_64(emitter: &mut Emitter) { // shifts the dispatch scratch frame by +16 only across the call; // we load r11 (stream_open ptr) BEFORE the sub so its [rsp+48] // reference is still valid. - emitter.instruction("mov rdi, QWORD PTR [rsp + 32]"); // $this = wrapper object - emitter.instruction("mov rsi, QWORD PTR [rsp + 0]"); // path ptr - emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // path len - emitter.instruction("mov rcx, QWORD PTR [rsp + 16]"); // mode ptr - emitter.instruction("mov r8, QWORD PTR [rsp + 24]"); // mode len - emitter.instruction("xor r9d, r9d"); // options = 0 (PHP STREAM_USE_PATH/REPORT_ERRORS unused in v1) - emitter.instruction("mov r11, QWORD PTR [rsp + 48]"); // reload stream_open method pointer before the stack shift - abi::emit_symbol_address(emitter, "r10", "_stream_open_opened_path_scratch"); // address of the 16-byte opened_path scratch slot - emitter.instruction("mov QWORD PTR [r10], 0"); // zero the opened_path low half before the call - emitter.instruction("mov QWORD PTR [r10 + 8], 0"); // zero the opened_path high half before the call - emitter.instruction("sub rsp, 16"); // reserve a 16-byte stack-arg slot for the 7th int arg (rsp stays 16-aligned) - emitter.instruction("mov QWORD PTR [rsp], r10"); // 7th arg (opened_path address) at [rsp+0] - emitter.instruction("call r11"); // invoke stream_open on the wrapper object - emitter.instruction("add rsp, 16"); // release the stack-arg slot + if emitter.target.platform == Platform::Windows && emitter.target.arch == Arch::X86_64 { + // windows-x86_64 (MSx64): the pre-shift [rsp+N] slots below are loaded + // BEFORE `sub rsp, 64` (they shift by 64 after the sub). args 0-3 go + // in rcx/rdx/r8/r9; args 4-6 (mode len, options, opened_path addr) + // are staged on the stack above the mandatory 32-byte shadow space + // (finding F1, wide reverse-ABI variant, 7-int-arg case). + emitter.instruction("mov rcx, QWORD PTR [rsp + 32]"); // $this (arg0) + emitter.instruction("mov rdx, QWORD PTR [rsp + 0]"); // path ptr (arg1) + emitter.instruction("mov r8, QWORD PTR [rsp + 8]"); // path len (arg2) + emitter.instruction("mov r9, QWORD PTR [rsp + 16]"); // mode ptr (arg3) + emitter.instruction("mov r11, QWORD PTR [rsp + 48]"); // stream_open ptr (call target) -- load before the shift + emitter.instruction("mov rax, QWORD PTR [rsp + 24]"); // mode len (arg4 scratch) -- load before the shift + abi::emit_symbol_address(emitter, "r10", "_stream_open_opened_path_scratch"); // opened_path addr (arg6) -- same call the SysV path uses (windows) + emitter.instruction("mov QWORD PTR [r10], 0"); // zero opened_path low half + emitter.instruction("mov QWORD PTR [r10 + 8], 0"); // zero opened_path high half + emitter.instruction("sub rsp, 64"); // 32B shadow + 3 stack args(24) + 8 pad (16-aligned) + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // arg4 = mode len + emitter.instruction("mov QWORD PTR [rsp + 40], 0"); // arg5 = options = 0 + emitter.instruction("mov QWORD PTR [rsp + 48], r10"); // arg6 = opened_path addr + emitter.instruction("call r11"); // invoke stream_open on the wrapper object (windows-x86_64 MSx64 stack-staged call) + emitter.instruction("add rsp, 64"); // release the windows arg-staging scratch + } else { + emitter.instruction("mov rdi, QWORD PTR [rsp + 32]"); // $this = wrapper object + emitter.instruction("mov rsi, QWORD PTR [rsp + 0]"); // path ptr + emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // path len + emitter.instruction("mov rcx, QWORD PTR [rsp + 16]"); // mode ptr + emitter.instruction("mov r8, QWORD PTR [rsp + 24]"); // mode len + emitter.instruction("xor r9d, r9d"); // options = 0 (PHP STREAM_USE_PATH/REPORT_ERRORS unused in v1) + emitter.instruction("mov r11, QWORD PTR [rsp + 48]"); // reload stream_open method pointer before the stack shift + abi::emit_symbol_address(emitter, "r10", "_stream_open_opened_path_scratch"); // address of the 16-byte opened_path scratch slot + emitter.instruction("mov QWORD PTR [r10], 0"); // zero the opened_path low half before the call + emitter.instruction("mov QWORD PTR [r10 + 8], 0"); // zero the opened_path high half before the call + emitter.instruction("sub rsp, 16"); // reserve a 16-byte stack-arg slot for the 7th int arg (rsp stays 16-aligned) + emitter.instruction("mov QWORD PTR [rsp], r10"); // 7th arg (opened_path address) at [rsp+0] + emitter.instruction("call r11"); // invoke stream_open on the wrapper object + emitter.instruction("add rsp, 16"); // release the stack-arg slot + } emitter.instruction("test rax, rax"); // did stream_open return false? emitter.instruction("jz __rt_fopen_uw_fail_x86"); // stream_open returned false → silent fail (obj is freed on the shared fail path) @@ -448,8 +561,18 @@ fn emit_fopen_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_fopen_opened_x86"); emitter.instruction("cdqe"); // normalize the successful C int fd into the runtime's 64-bit descriptor value - abi::emit_symbol_address(emitter, "r10", "_eof_flags"); // materialize the eof-flag table for the newly opened descriptor - emitter.instruction("mov BYTE PTR [r10 + rax], 0"); // clear stale EOF state before returning the descriptor + if emitter.target.platform == Platform::Windows { + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // preserve the returned CRT descriptor across slot lookup + emitter.instruction("mov rdi, rax"); // pass the descriptor to the opaque-handle registry + emitter.instruction("call __rt_win_stream_slot"); // obtain a bounded EOF-table slot + emitter.instruction("mov r11, rax"); // retain compact slot while materializing the table base + abi::emit_symbol_address(emitter, "r10", "_eof_flags"); // materialize the eof-flag table for the compact Windows slot + emitter.instruction("mov BYTE PTR [r10 + r11], 0"); // clear stale EOF state without raw descriptor indexing + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // return the original CRT descriptor to PHP code + } else { + abi::emit_symbol_address(emitter, "r10", "_eof_flags"); // materialize the eof-flag table for the newly opened descriptor + emitter.instruction("mov BYTE PTR [r10 + rax], 0"); // clear stale EOF state before returning the descriptor + } emitter.label("__rt_fopen_return_x86"); emitter.instruction("add rsp, 32"); // release the temporary pathname and mode spill slots before returning the file descriptor @@ -475,3 +598,66 @@ fn emit_fopen_failed_warning(emitter: &mut Emitter) { } } } + +#[cfg(test)] +mod tests { + use crate::codegen_support::emit::Emitter; + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::*; + + /// Verifies the windows-x86_64 `__rt_fopen` user-wrapper `stream_open` call + /// site stages the 7-int-arg MSx64 sequence (4 register args plus 3 stack + /// args: mode len, options, opened_path addr) above the mandatory 32-byte + /// shadow space before the indirect `call r11` (finding F1, wide reverse-ABI + /// variant, 7-int-arg case): without this staging the generated + /// `stream_open($this, $path, $mode, $options, &$opened_path)` method would + /// read its arguments from the wrong registers and stack slots on + /// windows-x86_64. + #[test] + fn test_windows_x86_64_fopen_stream_open_stages_stack_args_before_call() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_fopen(&mut emitter); + let asm = emitter.output(); + + let this_idx = asm.find("mov rcx, QWORD PTR [rsp + 32]").expect("expected MSx64 $this load"); + let opened_path_idx = asm + .find("mov QWORD PTR [rsp + 48], r10") + .expect("expected opened_path stack store"); + let call_idx = asm.find("call r11").expect("expected indirect call r11"); + assert!(this_idx < call_idx, "$this load must precede the indirect call"); + assert!(opened_path_idx < call_idx, "opened_path stack store must precede the indirect call"); + assert!(asm.contains("sub rsp, 64"), "expected the windows call-site stack-arg sub"); + assert_eq!( + asm.matches("add rsp, 64").count(), + 3, + "expected the windows call-site add plus the two shared wrapper-dispatch releases" + ); + } + + /// Verifies linux-x86_64 emission for the `__rt_fopen` user-wrapper + /// `stream_open` call site stays byte-identical to before the reverse-ABI + /// staging was introduced: the MSx64 stack staging is windows-x86_64-only, + /// so a linux-x86_64 build must keep the plain SysV 7-arg sequence (6 in + /// registers, the 7th on a 16-byte-aligned stack slot). + #[test] + fn test_linux_x86_64_fopen_stream_open_has_no_msx64_stack_staging() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_fopen(&mut emitter); + let asm = emitter.output(); + + assert!(!asm.contains("mov rcx, QWORD PTR [rsp + 32]")); + assert!(!asm.contains("mov QWORD PTR [rsp + 48], r10")); + assert!(asm.contains("mov rdi, QWORD PTR [rsp + 32]")); + assert_eq!( + asm.matches("sub rsp, 64").count(), + 1, + "expected only the shared wrapper-dispatch scratch sub on linux-x86_64" + ); + assert_eq!( + asm.matches("add rsp, 64").count(), + 2, + "expected only the two shared wrapper-dispatch releases on linux-x86_64" + ); + } +} diff --git a/src/codegen_support/runtime/io/format_sockaddr.rs b/src/codegen_support/runtime/io/format_sockaddr.rs index 352bdb7355..66631d1e3c 100644 --- a/src/codegen_support/runtime/io/format_sockaddr.rs +++ b/src/codegen_support/runtime/io/format_sockaddr.rs @@ -396,7 +396,7 @@ pub fn emit_format_sockaddr_in6(emitter: &mut Emitter) { /// Emits the Linux x86_64 stream runtime helper for format sockaddr in6. fn emit_format_sockaddr_in6_linux_x86_64(emitter: &mut Emitter) { - let af_inet6 = crate::codegen_support::platform::Platform::Linux.af_inet6(); + let af_inet6 = emitter.platform.af_inet6(); emitter.blank(); emitter.comment("--- runtime: format_sockaddr_in6 ---"); emitter.label_global("__rt_format_sockaddr_in6"); @@ -428,7 +428,7 @@ fn emit_format_sockaddr_in6_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("lea rdx, [rbp - 64]"); // out buffer (46-byte INET6_ADDRSTRLEN) emitter.instruction("mov ecx, 46"); // INET6_ADDRSTRLEN emitter.instruction(&format!("mov edi, {}", af_inet6)); // family: AF_INET6 - emitter.instruction("call inet_ntop"); // rax = buf pointer or NULL + emitter.emit_call_c("inet_ntop"); // rax = buf pointer or NULL emitter.instruction("test rax, rax"); // libc returned NULL? emitter.instruction("jz __rt_fsa6_fail_x86"); // bail on failure diff --git a/src/codegen_support/runtime/io/fread.rs b/src/codegen_support/runtime/io/fread.rs index 3a1533cf87..9037d9dbae 100644 --- a/src/codegen_support/runtime/io/fread.rs +++ b/src/codegen_support/runtime/io/fread.rs @@ -7,8 +7,28 @@ //! //! Key details: //! - I/O helpers bridge PHP strings, resources, descriptors, and libc calls while returning runtime arrays or pointer/length strings. +//! - `__rt_fread` returns a *borrowed* pointer/length pair. Reads that fit take a slice +//! of the 64 KiB concat arena, whose storage is recycled by the caller frame's +//! save/restore of `_concat_off`; reads that do not fit take an owned heap block +//! instead, so as not to run off the end of the arena. +//! - That block is reclaimed by the ordinary release path, which needs two things that +//! were both missing and are easy to mistake for one. The EIR already emits a +//! `release` for these values; the backend was discarding it, because +//! `value_is_scratch_string` treats any non-`Fresh` runtime call as arena scratch (see +//! `codegen/lower_inst/ownership.rs`). And the block carried heap kind 0, so every +//! `__rt_decref_any` in the runtime's own read paths skipped it as a raw block. Fixing +//! either alone changes nothing measurable, which is what made this look like a +//! missing lifetime mechanism rather than two independent omissions. +//! - Arena slices stay safe under that release: `__rt_heap_free_safe` validates against +//! the managed heap window, and `_concat_buf` is a separate `.comm` object that can +//! never fall inside it, so the arena case is an exact no-op rather than a heuristic. +//! - REMAINING GAP: a `stream_get_contents()` result that is boxed into a Mixed cell +//! still strands its grown accumulation buffer. The boxing path copies the bytes out +//! (`__rt_mixed_from_value`), so the original block is orphaned before any release +//! refers to it; only the Mixed cell is tracked. Closing that belongs to the boxing +//! path, not here. -use crate::codegen_support::{emit::Emitter, platform::Arch}; +use crate::codegen_support::{emit::Emitter, platform::{Arch, Platform}}; use crate::codegen_support::abi; /// Emits the `__rt_fread` runtime helper for reading bytes from a file descriptor. @@ -30,6 +50,8 @@ use crate::codegen_support::abi; /// - Advances `_concat_off` by actual bytes read. /// - Sets `_eof_flags[fd] = 1` when the stream is exhausted. pub fn emit_fread(emitter: &mut Emitter) { + emit_fread_readable_bytes(emitter); + if emitter.target.arch == Arch::X86_64 { emit_fread_linux_x86_64(emitter); return; @@ -56,34 +78,82 @@ pub fn emit_fread(emitter: &mut Emitter) { emitter.instruction("str x0, [sp, #0]"); // save file descriptor emitter.instruction("str x1, [sp, #8]"); // save requested read length - // -- get concat_buf write position -- + // -- pick the destination: the concat arena, or the heap when it will not fit -- + // The arena is a flat 64 KiB buffer with an offset and no capacity of its own, so + // a request larger than what is left of it used to be written straight past the + // end. Nothing reported that: the returned bytes were correct, because they were + // read back from the same overflowed region, while whatever followed the buffer + // was destroyed -- observed as a stream resource that fclose() then rejected as + // "unknown given", with the victim changing from program to program. + // + // Oversized reads go to their own heap block instead. Callers read it exactly as + // they read an arena slice -- same pointer/length pair -- which is what makes the + // substitution invisible to them. + // + // It is also why the block is never reclaimed: this helper's result is borrowed by + // contract, so no caller releases it, and the arena's own recycling (the frame + // save/restore of _concat_off) does not describe heap storage. Every read past the + // arena therefore strands its block for the life of the process. That is a bounded + // leak where the unbounded write it replaced was silent corruption, so it stands + // until the lifetime is modelled properly -- see the note in the module docs. + // + // That block is sized from the stream, never from the request: fread() reads *up + // to* the length it is given, so `fread($f, 100000000)` on a small file is + // ordinary PHP that php answers in full, and reserving the asked-for size instead + // emptied the 8 MiB heap in one call. The clamped length is stored back, because + // both the TLS path and the read syscall below take their count from it. crate::codegen_support::abi::emit_symbol_address(emitter, "x9", "_concat_off"); emitter.instruction("ldr x10, [x9]"); // load current write offset + emitter.instruction("mov x13, #65536"); // the arena's fixed capacity + emitter.instruction("sub x13, x13, x10"); // bytes still free in the arena + emitter.instruction("ldr x14, [sp, #8]"); // the requested read length + emitter.instruction("cmp x14, x13"); // does the whole request still fit? + emitter.instruction("b.ls __rt_fread_dest_choose"); // it does: no reason to probe the stream + emitter.instruction("ldr x0, [sp, #0]"); // the descriptor to probe + emitter.instruction("bl __rt_fread_readable_bytes"); // x0 = what this stream can still supply + emitter.instruction("ldr x14, [sp, #8]"); // reload the request across the probe + emitter.instruction("cmp x14, x0"); // is the request beyond that supply? + emitter.instruction("csel x14, x0, x14, hi"); // read only what the stream actually holds + emitter.instruction("str x14, [sp, #8]"); // the read and the TLS path share this length + crate::codegen_support::abi::emit_symbol_address(emitter, "x9", "_concat_off"); + emitter.instruction("ldr x10, [x9]"); // reload the offset the probe clobbered + emitter.instruction("mov x13, #65536"); // the arena's fixed capacity + emitter.instruction("sub x13, x13, x10"); // bytes still free in the arena + emitter.label("__rt_fread_dest_choose"); crate::codegen_support::abi::emit_symbol_address(emitter, "x11", "_concat_buf"); + emitter.instruction("cmp x14, x13"); // does the clamped read still fit? + emitter.instruction("b.hi __rt_fread_dest_heap"); // it does not: take an owned block emitter.instruction("add x12, x11, x10"); // compute write pointer: buf + offset + emitter.instruction("b __rt_fread_dest_ready"); // the arena is the destination + emitter.label("__rt_fread_dest_heap"); + emitter.instruction("mov x0, x14"); // allocate what the stream can supply + emitter.instruction("bl __rt_heap_alloc"); // x0 = owned payload pointer + emitter.instruction("mov x13, #1"); // heap kind 1 = owned elephc string + emitter.instruction("str x13, [x0, #-8]"); // stamp it: raw blocks are skipped by every decref + emitter.instruction("mov x12, x0"); // the heap block is the destination + emitter.label("__rt_fread_dest_ready"); emitter.instruction("str x12, [sp, #16]"); // save start pointer for return value // -- TLS dispatch: route through elephc_tls_read when fd has an // attached session (Phase 11 B3). -- emitter.instruction("ldr x0, [sp, #0]"); // reload fd for the TLS check - crate::codegen_support::abi::emit_symbol_address(emitter, "x13", "_tls_sessions"); - emitter.instruction("ldr x14, [x13, x0, lsl #3]"); // _tls_sessions[fd] handle (0 = plain TCP) - emitter.instruction("cbz x14, __rt_fread_do_syscall"); // no TLS attached → fall through to read syscall - emitter.instruction("mov x0, x14"); // handle as first arg - emitter.instruction("mov x1, x12"); // buf ptr + emitter.instruction("bl __rt_tls_session_get"); // resolve the full-width descriptor through the bounded TLS map + emitter.instruction("cbz x0, __rt_fread_do_syscall"); // no TLS attached → fall through to read syscall + emitter.instruction("ldr x1, [sp, #16]"); // reload the concat-buffer destination pointer emitter.instruction("ldr x2, [sp, #8]"); // len crate::codegen_support::abi::emit_symbol_address(emitter, "x9", "_elephc_tls_read_fn"); emitter.instruction("ldr x9, [x9]"); // load elephc_tls_read entry pointer - emitter.instruction("blr x9"); // x0 = bytes read (>=0) or -1 - emitter.instruction("cmp x0, #0"); // value-based check after the TLS call - emitter.instruction("b.ge __rt_fread_read_ok"); // continue when TLS read returned >= 0 - emitter.instruction("str xzr, [sp, #24]"); // TLS error: zero-length result - emitter.instruction("b __rt_fread_mark_eof"); // mark the stream exhausted + emitter.emit_published_bridge_call("x9"); // x0 = bytes, EOF, or one documented TLS v2 sentinel + emitter.instruction("cmp x0, #-1"); // terminal TLS failures alone retain the existing EOF behavior + emitter.instruction("b.eq __rt_fread_mark_eof"); // terminal TLS failure: report an empty exhausted stream + emitter.instruction("cmp x0, #0"); // distinguish EOF/bytes from retryable TLS sentinels + emitter.instruction("b.ge __rt_fread_read_ok"); // bytes and TLS EOF use the normal read-result path + emitter.instruction("b __rt_fread_would_block"); // WouldBlock/TimedOut return empty without marking EOF emitter.label("__rt_fread_do_syscall"); // -- perform read syscall -- emitter.instruction("ldr x0, [sp, #0]"); // fd for read syscall - emitter.instruction("mov x1, x12"); // buffer pointer for read + emitter.instruction("ldr x1, [sp, #16]"); // reload the buffer pointer after the TLS lookup helper emitter.instruction("ldr x2, [sp, #8]"); // number of bytes to read emitter.syscall(3); if emitter.platform.needs_cmp_before_error_branch() { @@ -100,12 +170,23 @@ pub fn emit_fread(emitter: &mut Emitter) { emitter.instruction("b __rt_fread_mark_eof"); // mark the stream as exhausted after a read failure emitter.label("__rt_fread_read_ok"); - // -- update concat_off by actual bytes read -- + // -- update concat_off by actual bytes read, but only for an arena destination -- + // An oversized read landed in its own heap block, which the arena offset does not + // describe; advancing it there would hand the next writer a cursor into bytes + // nobody wrote. The destination pointer says which case this is: an unsigned + // distance below the arena's capacity means it points inside it. emitter.instruction("str x0, [sp, #24]"); // save actual bytes read + emitter.instruction("ldr x12, [sp, #16]"); // reload the destination pointer + crate::codegen_support::abi::emit_symbol_address(emitter, "x11", "_concat_buf"); + emitter.instruction("sub x13, x12, x11"); // distance from the arena base + emitter.instruction("mov x14, #65536"); // the arena's fixed capacity + emitter.instruction("cmp x13, x14"); // did the bytes land inside the arena? + emitter.instruction("b.hs __rt_fread_off_kept"); // heap destination: the arena cursor is unrelated crate::codegen_support::abi::emit_symbol_address(emitter, "x9", "_concat_off"); emitter.instruction("ldr x10, [x9]"); // load current offset emitter.instruction("add x10, x10, x0"); // advance offset by bytes read emitter.instruction("str x10, [x9]"); // store updated offset + emitter.label("__rt_fread_off_kept"); // -- set eof flag if read returned 0 -- emitter.instruction("ldr x0, [sp, #24]"); // reload bytes read @@ -168,26 +249,71 @@ fn emit_fread_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the file descriptor across the concat-buffer address computation and libc read() call emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // preserve the requested byte count across the concat-buffer address computation and libc read() call + // Same arena-overflow guard as the AArch64 arm above: a request larger than what + // is left of the flat 64 KiB arena gets its own heap block instead of running off + // the end of it. Callers cannot tell the difference -- a heap-backed string is the + // same pointer/length pair, and __rt_decref_any validates against the managed heap + // window, so it releases this block and keeps ignoring arena slices. + // + // The block is sized from the stream and not from the request, for the reason the + // AArch64 arm spells out: fread() reads *up to* its length, so an oversized + // request is ordinary PHP and must not reserve what was merely asked for. abi::emit_load_symbol_to_reg(emitter, "r10", "_concat_off", 0); // load the current concat-buffer absolute offset before appending the fread() result + emitter.instruction("mov rax, 65536"); // the arena's fixed capacity + emitter.instruction("sub rax, r10"); // bytes still free in the arena + emitter.instruction("cmp QWORD PTR [rbp - 16], rax"); // does the whole request still fit? + emitter.instruction("jbe __rt_fread_dest_choose_x86"); // it does: no reason to probe the stream + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // the descriptor to probe + emitter.instruction("call __rt_fread_readable_bytes"); // rax = what this stream can still supply + emitter.instruction("cmp QWORD PTR [rbp - 16], rax"); // is the request beyond that supply? + emitter.instruction("jbe __rt_fread_len_kept_x86"); // the request already fits inside it + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // read only what the stream actually holds + emitter.label("__rt_fread_len_kept_x86"); + abi::emit_load_symbol_to_reg(emitter, "r10", "_concat_off", 0); // reload the offset the probe clobbered + emitter.instruction("mov rax, 65536"); // the arena's fixed capacity + emitter.instruction("sub rax, r10"); // bytes still free in the arena + emitter.label("__rt_fread_dest_choose_x86"); abi::emit_symbol_address(emitter, "r11", "_concat_buf"); // materialize the concat-buffer base address once for the x86_64 fread() helper + emitter.instruction("cmp QWORD PTR [rbp - 16], rax"); // does the clamped read still fit? + emitter.instruction("ja __rt_fread_dest_heap_x86"); // it does not: take an owned block emitter.instruction("lea rax, [r11 + r10]"); // compute the start pointer for the bytes that libc read() will append + emitter.instruction("jmp __rt_fread_dest_ready_x86"); // the arena is the destination + emitter.label("__rt_fread_dest_heap_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // allocate what the stream can supply + emitter.instruction("call __rt_heap_alloc"); // rax = owned payload pointer + emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(1))); // owned-string kind word carrying the x86_64 heap magic + emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp it: raw blocks are skipped by every decref + emitter.label("__rt_fread_dest_ready_x86"); emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // preserve the concat-buffer start pointer for the final elephc string result + if emitter.target.platform == Platform::Windows { + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // restore the opaque descriptor before starting a new read operation + emitter.instruction("call __rt_win_stream_clear_timed_out"); // every read begins with timed_out=false, including TLS-dispatched reads + } + // -- TLS dispatch: route through elephc_tls_read when fd has an // attached session (Phase 11 B3). -- - emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload fd for the TLS table lookup - abi::emit_symbol_address(emitter, "r11", "_tls_sessions"); // load runtime data address - emitter.instruction("mov r12, QWORD PTR [r11 + r10 * 8]"); // _tls_sessions[fd] handle (0 = plain TCP) - emitter.instruction("test r12, r12"); // check whether the runtime value is zero + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the full-width descriptor for the TLS map lookup + emitter.instruction("call __rt_tls_session_get"); // resolve the descriptor without using it as a raw array index + emitter.instruction("test rax, rax"); // check whether the runtime value is zero emitter.instruction("jz __rt_fread_do_syscall_x86"); // no TLS attached → use libc read - emitter.instruction("mov rdi, r12"); // handle as first arg + emitter.instruction("mov rdi, rax"); // handle as first arg emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // buf ptr emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // len abi::emit_load_symbol_to_reg(emitter, "r9", "_elephc_tls_read_fn", 0); // prepare SysV call argument - emitter.instruction("call r9"); // rax = bytes read (>=0) or -1 - emitter.instruction("cmp rax, 0"); // did the TLS bridge return bytes? - emitter.instruction("jle __rt_fread_eof_x86"); // TLS errors and EOF still mark the stream as exhausted - emitter.instruction("jmp __rt_fread_read_ok_x86"); // publish the successful TLS read + emitter.emit_published_bridge_call("r9"); // rax = bytes, EOF, or one documented TLS v2 sentinel + emitter.instruction("cmp rax, -1"); // terminal TLS failure alone retains the existing EOF behavior + emitter.instruction("je __rt_fread_eof_x86"); // terminal TLS failure: report an empty exhausted stream + emitter.instruction("cmp rax, 0"); // distinguish EOF/bytes from retryable TLS sentinels + emitter.instruction("jg __rt_fread_read_ok_x86"); // publish a positive TLS read + emitter.instruction("je __rt_fread_eof_x86"); // a zero-byte TLS read is genuine EOF + if emitter.target.platform == Platform::Windows { + emitter.instruction("cmp rax, -3"); // TLS v2 TimedOut needs PHP-visible Windows stream metadata + emitter.instruction("jne __rt_fread_would_block_x86"); // TLS WouldBlock remains an empty non-EOF read + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // restore the opaque stream descriptor for timeout metadata + emitter.instruction("call __rt_win_stream_mark_timed_out"); // record TimedOut without mutating EOF state + } + emitter.instruction("jmp __rt_fread_would_block_x86"); // retryable TLS sentinels return an empty non-EOF read emitter.label("__rt_fread_do_syscall_x86"); emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // pass the file descriptor as the first libc read() argument emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // pass the concat-buffer write pointer as the second libc read() argument @@ -199,19 +325,38 @@ fn emit_fread_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jmp __rt_fread_eof_x86"); // zero-byte read means real EOF emitter.label("__rt_fread_read_ok_x86"); + // Only an arena destination moves the arena cursor: an oversized read landed in + // its own heap block, which that cursor does not describe, and advancing it there + // would hand the next writer an offset into bytes nobody wrote. The destination + // pointer says which case this is -- an unsigned distance below the capacity means + // it points inside the arena. + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the destination pointer + abi::emit_symbol_address(emitter, "r10", "_concat_buf"); // the arena base + emitter.instruction("sub r11, r10"); // distance from the arena base + emitter.instruction("cmp r11, 65536"); // did the bytes land inside the arena? + emitter.instruction("jae __rt_fread_off_kept_x86"); // heap destination: the arena cursor is unrelated abi::emit_load_symbol_to_reg(emitter, "r10", "_concat_off", 0); // reload the previous concat-buffer absolute offset before publishing the fread() append emitter.instruction("add r10, rax"); // advance the concat-buffer offset by the number of bytes libc read() returned abi::emit_store_reg_to_symbol(emitter, "r10", "_concat_off", 0); // publish the updated concat-buffer offset for later string appenders + emitter.label("__rt_fread_off_kept_x86"); emitter.instruction("mov rdx, rax"); // return the successful byte count in the x86_64 elephc string-length result register emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // return the concat-buffer start pointer in the x86_64 elephc string-pointer result register emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the file descriptor for the read-filter lookup + if emitter.target.platform == Platform::Windows { + emitter.instruction("mov QWORD PTR [rbp - 32], rdx"); // preserve the successful byte count across the slot-registry call + emitter.instruction("mov rdi, r10"); // pass the opaque Windows descriptor to the slot registry + emitter.instruction("call __rt_win_stream_slot"); // obtain a bounded filter-table slot + emitter.instruction("mov r10, rax"); // table indexing uses the compact slot, never a raw SOCKET + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // restore the fread result pointer clobbered by the slot lookup + emitter.instruction("mov rdx, QWORD PTR [rbp - 32]"); // restore the fread result length clobbered by the slot lookup + } abi::emit_symbol_address(emitter, "r11", "_stream_read_filters"); // materialize the read-filter table base - emitter.instruction("movzx ecx, BYTE PTR [r11 + r10]"); // read filter id for this descriptor + emitter.instruction("movzx ecx, BYTE PTR [r11 + r10]"); // read filter id for this bounded stream slot emitter.instruction("test rcx, rcx"); // is a read filter attached to this stream? emitter.instruction("jz __rt_fread_ret_x86"); // skip when no read filter is attached emitter.instruction("cmp rcx, 128"); // user-filter id range (>= USER_FILTER_ID_BASE)? emitter.instruction("jl __rt_fread_builtin_filter_x86"); // built-in filter: in-place transform - emitter.instruction("mov rdi, r10"); // fd into the user-filter dispatcher's first arg + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // retain the real descriptor for user-filter dispatch emitter.instruction("mov rsi, rax"); // buf ptr into the dispatcher's second arg // rdx already holds the byte count emitter.instruction("xor ecx, ecx"); // direction = 0 (read) for the user-filter dispatch @@ -229,6 +374,13 @@ fn emit_fread_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov r10d, DWORD PTR [rax]"); // load the thread-local errno value emitter.instruction("cmp r10d, 11"); // is this EAGAIN/EWOULDBLOCK from a nonblocking fd? emitter.instruction("je __rt_fread_would_block_x86"); // transient nonblocking miss returns empty without EOF + if emitter.target.platform == Platform::Windows { + emitter.instruction("cmp r10d, 110"); // ETIMEDOUT is retryable stream timeout, not EOF + emitter.instruction("jne __rt_fread_eof_x86"); // only genuine non-timeout failures preserve the old EOF behavior + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // restore the opaque stream descriptor for timeout metadata + emitter.instruction("call __rt_win_stream_mark_timed_out"); // record timed_out without touching EOF state + emitter.instruction("jmp __rt_fread_would_block_x86"); // return an empty non-EOF read after timeout + } emitter.instruction("jmp __rt_fread_eof_x86"); // other read failures behave like an exhausted stream emitter.label("__rt_fread_would_block_x86"); @@ -239,12 +391,331 @@ fn emit_fread_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("ret"); // return the empty non-EOF read result emitter.label("__rt_fread_eof_x86"); + // The destination is chosen before the read, so an oversized read that turns out + // to be at end of stream has already taken its block -- and this arm returns a + // null pointer, so nothing downstream can release it. A drain loop reading large + // chunks ends on exactly this path, once per stream. __rt_heap_free ignores + // pointers without the heap marker, so an arena slice passes through untouched + // and needs no test here. The AArch64 arm returns the pointer with length 0 + // instead of dropping it, so its caller still owns the block and it must not be + // released there. + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // the destination chosen before the read + emitter.instruction("call __rt_heap_free"); // release it when the read never filled it emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the file descriptor so the eof-flag table can mark this stream as exhausted + if emitter.target.platform == Platform::Windows { + emitter.instruction("mov rdi, r10"); // pass the opaque Windows descriptor to the slot registry + emitter.instruction("call __rt_win_stream_slot"); // obtain a bounded EOF-table slot + emitter.instruction("mov r10, rax"); // table indexing uses the compact slot, never a raw SOCKET + } abi::emit_symbol_address(emitter, "r11", "_eof_flags"); // materialize the eof-flag table base address for the current stream descriptor - emitter.instruction("mov BYTE PTR [r11 + r10], 1"); // mark the current file descriptor as EOF-reached after the zero-byte or failed read + emitter.instruction("mov BYTE PTR [r11 + r10], 1"); // mark the compact stream slot as EOF-reached after the zero-byte or failed read emitter.instruction("xor eax, eax"); // return an empty string pointer when libc read() reports EOF or failure emitter.instruction("xor edx, edx"); // return an empty string length when libc read() reports EOF or failure emitter.instruction("add rsp, 32"); // release the fread() spill slots before returning the empty-string result emitter.instruction("pop rbp"); // restore the caller frame pointer after the EOF/error fread() path emitter.instruction("ret"); // return the empty string result for the exhausted or failed stream read } + +/// Emits `__rt_fread_readable_bytes`, the bound used to size an oversized read. +/// +/// `fread($f, $n)` reads *up to* `$n` bytes, so a request may legitimately dwarf the +/// stream behind it: `fread($f, 100000000)` on a 13-byte file is 13 bytes in PHP, not +/// an error. Sizing an owned block from the request alone therefore emptied elephc's +/// 8 MiB default heap on a read php answers in full, which is what this bound exists +/// to prevent. +/// +/// The probe is `lseek` rather than `fstat` because it has to tell a file from a +/// socket, and only `lseek` *fails* on one (ESPIPE). `fstat` succeeds on a socket and +/// reports `st_size` 0, which would clamp every socket read to nothing. A stream that +/// cannot be asked falls back to one arena's worth: php stops such a read at the first +/// packet regardless, so a larger bound would buy nothing. +/// +/// # Inputs +/// - x0/rdi: file descriptor +/// +/// # Outputs +/// - x0/rax: bytes the stream can still supply, or 65536 when it cannot be asked +/// +/// # Side effects +/// - None. The seek position is restored before returning. +fn emit_fread_readable_bytes(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_fread_readable_bytes_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: fread readable bytes ---"); + emitter.label_global("__rt_fread_readable_bytes"); + emitter.instruction("sub sp, sp, #48"); // allocate the probe frame + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #32"); // establish the probe frame pointer + emitter.instruction("str x0, [sp, #0]"); // keep the descriptor across the seek probes + + // -- where the stream is positioned: lseek(fd, 0, SEEK_CUR) -- + emitter.instruction("mov x1, #0"); // offset 0 + emitter.instruction("mov x2, #1"); // SEEK_CUR + emitter.syscall(199); + if emitter.platform.needs_cmp_before_error_branch() { + emitter.instruction("cmp x0, #0"); // Linux: a negative result means lseek failed + } + emitter.instruction(&emitter.platform.branch_on_syscall_success("__rt_frb_seekable")); // the size is knowable + emitter.instruction("mov x0, #65536"); // a socket or pipe cannot be asked: one arena's worth + emitter.instruction("b __rt_frb_ret"); // nothing moved, so nothing to restore + + emitter.label("__rt_frb_seekable"); + emitter.instruction("str x0, [sp, #8]"); // remember where the read starts from + + // -- where the stream ends: lseek(fd, 0, SEEK_END) -- + emitter.instruction("ldr x0, [sp, #0]"); // reload the descriptor + emitter.instruction("mov x1, #0"); // offset 0 + emitter.instruction("mov x2, #2"); // SEEK_END + emitter.syscall(199); + if emitter.platform.needs_cmp_before_error_branch() { + emitter.instruction("cmp x0, #0"); // Linux: a negative result means lseek failed + } + emitter.instruction(&emitter.platform.branch_on_syscall_success("__rt_frb_have_end")); // the end offset is usable + emitter.instruction("mov x0, #65536"); // the end is unknown: fall back to one arena's worth + emitter.instruction("b __rt_frb_ret"); // the position never moved, so nothing to restore + + emitter.label("__rt_frb_have_end"); + emitter.instruction("str x0, [sp, #16]"); // hold the end across the restoring seek + emitter.instruction("ldr x0, [sp, #0]"); // reload the descriptor + emitter.instruction("ldr x1, [sp, #8]"); // seek back to where the stream was + emitter.instruction("mov x2, #0"); // SEEK_SET + emitter.syscall(199); + emitter.instruction("ldr x9, [sp, #16]"); // the end offset + emitter.instruction("ldr x10, [sp, #8]"); // the offset the read starts from + emitter.instruction("subs x0, x9, x10"); // bytes between the position and the end + emitter.instruction("b.gt __rt_frb_ret"); // a positive remainder is the bound + emitter.instruction("mov x0, #0"); // at or past the end: nothing left to read + + emitter.label("__rt_frb_ret"); + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // deallocate the probe frame + emitter.instruction("ret"); // return the supply bound +} + +/// Emits the x86_64 variant of `__rt_fread_readable_bytes`. +/// +/// Same three-step seek probe as the AArch64 arm, issued as raw `lseek` syscalls so +/// `windows_transform` rewrites them into `__rt_sys_lseek`, whose `SetFilePointerEx` +/// fails on a socket handle exactly as ESPIPE does on Linux. The descriptor is +/// reloaded before every probe because that rewrite turns each one into a call, which +/// keeps none of the argument registers. +fn emit_fread_readable_bytes_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: fread readable bytes ---"); + emitter.label_global("__rt_fread_readable_bytes"); + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the probe + emitter.instruction("sub rsp, 32"); // reserve the descriptor, position and end slots + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // keep the descriptor across the seek probes + + // -- where the stream is positioned: lseek(fd, 0, SEEK_CUR) -- + emitter.instruction("xor esi, esi"); // offset 0 + emitter.instruction("mov edx, 1"); // SEEK_CUR + emitter.instruction("mov eax, 8"); // Linux x86_64 syscall 8 = lseek + emitter.instruction("syscall"); // probe whether the descriptor is seekable + emitter.instruction("test rax, rax"); // did lseek fail with a negative result? + emitter.instruction("jns __rt_frb_seekable_x86"); // seekable: the size is knowable + emitter.instruction("mov rax, 65536"); // a socket or pipe cannot be asked: one arena's worth + emitter.instruction("jmp __rt_frb_ret_x86"); // nothing moved, so nothing to restore + + emitter.label("__rt_frb_seekable_x86"); + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // remember where the read starts from + + // -- where the stream ends: lseek(fd, 0, SEEK_END) -- + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the descriptor + emitter.instruction("xor esi, esi"); // offset 0 + emitter.instruction("mov edx, 2"); // SEEK_END + emitter.instruction("mov eax, 8"); // Linux x86_64 syscall 8 = lseek + emitter.instruction("syscall"); // query the end of the stream + emitter.instruction("test rax, rax"); // did lseek fail with a negative result? + emitter.instruction("jns __rt_frb_have_end_x86"); // the end offset is usable + emitter.instruction("mov rax, 65536"); // the end is unknown: fall back to one arena's worth + emitter.instruction("jmp __rt_frb_ret_x86"); // the position never moved, so nothing to restore + + emitter.label("__rt_frb_have_end_x86"); + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // hold the end across the restoring seek + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the descriptor + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // seek back to where the stream was + emitter.instruction("xor edx, edx"); // SEEK_SET + emitter.instruction("mov eax, 8"); // Linux x86_64 syscall 8 = lseek + emitter.instruction("syscall"); // restore the position the probe consumed + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // the end offset + emitter.instruction("sub rax, QWORD PTR [rbp - 16]"); // bytes between the position and the end + emitter.instruction("jg __rt_frb_ret_x86"); // a positive remainder is the bound + emitter.instruction("xor eax, eax"); // at or past the end: nothing left to read + + emitter.label("__rt_frb_ret_x86"); + emitter.instruction("add rsp, 32"); // release the probe slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the supply bound +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::codegen::platform::Target; + + /// Verifies both arms send an oversized read to the heap, not past the arena. + /// + /// The concat arena is a flat 64 KiB buffer with an offset and no capacity of its + /// own, so a larger request used to be written straight off the end of it. The + /// fixture that proves the behaviour needs a real filesystem and a linker, and the + /// windows link set is not reproducible on every host, so the guard that both arms + /// carry the check lives here. + #[test] + fn oversized_reads_take_an_owned_block_instead_of_overrunning_the_arena() { + for (platform, arch, alloc, guard) in [ + (Platform::MacOS, Arch::AArch64, "bl __rt_heap_alloc\n", "__rt_fread_dest_heap:\n"), + (Platform::Linux, Arch::X86_64, "call __rt_heap_alloc\n", "__rt_fread_dest_heap_x86:\n"), + ] { + let mut emitter = Emitter::new(Target::new(platform, arch)); + emit_fread(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains(guard), "{:?}/{:?}: missing the capacity guard", platform, arch); + assert!(asm.contains(alloc), "{:?}/{:?}: the guard must allocate", platform, arch); + assert!( + asm.matches("65536").count() >= 2, + "{:?}/{:?}: the capacity is needed both to choose the destination and to \ + decide whether the arena cursor moves", + platform, + arch + ); + } + } + + /// Verifies an oversized read is sized from the stream, never from the request. + /// + /// `fread($f, $n)` reads up to `$n` bytes, so the request is an upper bound and + /// not an amount to reserve: sizing the block from it turned `fread($f, 1e8)` on + /// a small file -- which php answers in full -- into an exhausted 8 MiB heap. The + /// probe has to run *before* the allocation for the clamp to mean anything, which + /// is the ordering this pins. + #[test] + fn oversized_reads_are_sized_from_the_stream_and_not_from_the_request() { + for (platform, arch, alloc) in [ + (Platform::MacOS, Arch::AArch64, "bl __rt_heap_alloc"), + (Platform::Linux, Arch::X86_64, "call __rt_heap_alloc"), + (Platform::Windows, Arch::X86_64, "call __rt_heap_alloc"), + ] { + let mut emitter = Emitter::new(Target::new(platform, arch)); + emit_fread(&mut emitter); + let asm = emitter.output(); + + let fread_body = asm + .find("__rt_fread:") + .expect("missing the fread entry point"); + let probe = asm[fread_body..] + .find("__rt_fread_readable_bytes") + .unwrap_or_else(|| panic!("{:?}/{:?}: the read is never bounded by the stream", platform, arch)); + let allocation = asm[fread_body..] + .find(alloc) + .unwrap_or_else(|| panic!("{:?}/{:?}: the oversized path must still take a block", platform, arch)); + assert!( + probe < allocation, + "{:?}/{:?}: the block is sized before the stream is asked what it holds", + platform, + arch + ); + } + } + + /// Verifies the probe leaves the stream position exactly where it found it. + /// + /// It reads the position, seeks to the end to measure, and must seek back: a + /// probe that skipped the third seek would silently consume the whole stream and + /// hand back nothing, which no fixture asserting only on lengths would catch. + #[test] + fn the_readable_bytes_probe_restores_the_stream_position() { + for (platform, arch, seek, expected) in [ + (Platform::MacOS, Arch::AArch64, "mov x16, #199", 3), + (Platform::Linux, Arch::X86_64, "mov eax, 8", 3), + ] { + let mut emitter = Emitter::new(Target::new(platform, arch)); + emit_fread_readable_bytes(&mut emitter); + let asm = emitter.output(); + + assert_eq!( + asm.matches(seek).count(), + expected, + "{:?}/{:?}: the probe must read the position, measure the end, and seek back", + platform, + arch + ); + } + } + + /// Verifies the x86_64 EOF arm releases a block its own null return would strand. + /// + /// The destination is chosen before the read, so an oversized read that turns out + /// to be at end of stream has already taken a block -- and this arm returns a null + /// pointer, leaving nothing downstream able to release it. A drain loop reading + /// large chunks ends on exactly this path. The AArch64 arm hands the pointer back + /// with length 0 instead, so its caller still owns the block and freeing it there + /// would be a use-after-free. + #[test] + fn an_oversized_read_at_end_of_stream_releases_its_block_on_x86_64() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_fread(&mut emitter); + let asm = emitter.output(); + let eof = asm + .find("__rt_fread_eof_x86:") + .expect("missing the x86_64 EOF arm"); + let release = asm[eof..] + .find("call __rt_heap_free") + .expect("the EOF arm strands the block it allocated before the read"); + let returns = asm[eof..].find("ret").expect("missing the EOF return"); + assert!(release < returns, "the block must be released before returning"); + + let mut arm = Emitter::new(Target::new(Platform::MacOS, Arch::AArch64)); + emit_fread(&mut arm); + let arm_asm = arm.output(); + let mark_eof = arm_asm + .find("__rt_fread_mark_eof:") + .expect("missing the AArch64 EOF arm"); + assert!( + !arm_asm[mark_eof..].contains("bl __rt_heap_free"), + "AArch64 returns the pointer to its caller, so releasing it here frees a live block" + ); + } + + /// Verifies TLS v2 retry sentinels reach the non-EOF path on both active + /// runtime architectures, while only the terminal sentinel reaches EOF. + #[test] + fn tls_retry_sentinels_do_not_emit_eof_branches() { + let mut arm = Emitter::new(Target::new(Platform::MacOS, Arch::AArch64)); + emit_fread(&mut arm); + let arm_asm = arm.output(); + assert!(arm_asm.contains("cmp x0, #-1")); + assert!(arm_asm.contains("b.eq __rt_fread_mark_eof")); + assert!(arm_asm.contains("b __rt_fread_would_block")); + + let mut x86 = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_fread(&mut x86); + let x86_asm = x86.output(); + assert!(x86_asm.contains("cmp rax, -1")); + assert!(x86_asm.contains("je __rt_fread_eof_x86")); + assert!(x86_asm.contains("jmp __rt_fread_would_block_x86")); + } + + /// Verifies Windows keeps the successful fread string result intact while + /// mapping an opaque descriptor to its bounded filter-table slot. + #[test] + fn windows_filter_slot_lookup_preserves_fread_result() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_fread(&mut emitter); + let asm = emitter.output(); + let slot_call = asm.find("call __rt_win_stream_slot").expect("missing slot lookup"); + let restore_ptr = asm[slot_call..] + .find("mov rax, QWORD PTR [rbp - 24]") + .expect("missing fread pointer restore"); + let restore_len = asm[slot_call..] + .find("mov rdx, QWORD PTR [rbp - 32]") + .expect("missing fread length restore"); + assert!(restore_ptr < restore_len); + } +} diff --git a/src/codegen_support/runtime/io/fsockopen.rs b/src/codegen_support/runtime/io/fsockopen.rs index 25e1525016..14c51091e9 100644 --- a/src/codegen_support/runtime/io/fsockopen.rs +++ b/src/codegen_support/runtime/io/fsockopen.rs @@ -35,10 +35,38 @@ pub fn emit_fsockopen(emitter: &mut Emitter) { emitter.instruction("str x1, [sp, #8]"); // save the hostname length emitter.instruction("str x2, [sp, #16]"); // save the port - // -- copy the "tcp://" scheme prefix into the address buffer -- + // -- prepend the default "tcp://" transport only when the hostname has none -- + // php formats fsockopen's address as "%s:%d" and hands it to + // php_stream_xport_create, which parses a leading "scheme://" and defaults to + // tcp only when the hostname carries no transport of its own + // (ext/standard/fsock.c). Prefixing unconditionally turned the documented + // fsockopen("tls://host", 443) form into "tcp://tls://host:443", which cannot + // resolve, so every prefixed hostname failed to connect. abi::emit_symbol_address(emitter, "x3", "_fsockopen_addr"); - abi::emit_symbol_address(emitter, "x4", "_ftp_tcp_prefix"); emitter.instruction("mov x5, #0"); // address write index + emitter.instruction("ldr x4, [sp, #0]"); // hostname pointer + emitter.instruction("ldr x7, [sp, #8]"); // hostname byte length + emitter.instruction("mov x6, #0"); // scheme scan index + emitter.label("__rt_fsockopen_scheme"); + emitter.instruction("add x8, x6, #2"); // index the second '/' would occupy + emitter.instruction("cmp x8, x7"); // does a "://" marker still fit? + emitter.instruction("b.ge __rt_fsockopen_no_scheme"); // no transport prefix - php defaults to tcp + emitter.instruction("ldrb w9, [x4, x6]"); // load the candidate ':' + emitter.instruction("cmp w9, #58"); // is it ':'? + emitter.instruction("b.ne __rt_fsockopen_scheme_next"); // not the marker - keep scanning + emitter.instruction("add x8, x6, #1"); // index of the first '/' + emitter.instruction("ldrb w9, [x4, x8]"); // load the candidate first '/' + emitter.instruction("cmp w9, #47"); // is it '/'? + emitter.instruction("b.ne __rt_fsockopen_scheme_next"); // not the marker - keep scanning + emitter.instruction("add x8, x6, #2"); // index of the second '/' + emitter.instruction("ldrb w9, [x4, x8]"); // load the candidate second '/' + emitter.instruction("cmp w9, #47"); // is it '/'? + emitter.instruction("b.eq __rt_fsockopen_pfx_done"); // hostname has its own transport - copy it verbatim + emitter.label("__rt_fsockopen_scheme_next"); + emitter.instruction("add x6, x6, #1"); // advance the scheme scan index + emitter.instruction("b __rt_fsockopen_scheme"); // keep looking for "://" + emitter.label("__rt_fsockopen_no_scheme"); + abi::emit_symbol_address(emitter, "x4", "_ftp_tcp_prefix"); emitter.label("__rt_fsockopen_pfx"); emitter.instruction("cmp x5, #6"); // copied the whole \"tcp://\" prefix? emitter.instruction("b.ge __rt_fsockopen_pfx_done"); // prefix copied @@ -108,10 +136,29 @@ fn emit_fsockopen_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the hostname length emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the port - // -- copy the "tcp://" scheme prefix into the address buffer -- + // -- prepend the default "tcp://" transport only when the hostname has none -- + // See the AArch64 arm: php lets fsockopen's hostname carry its own transport + // and only falls back to tcp when it does not. abi::emit_symbol_address(emitter, "r8", "_fsockopen_addr"); // address buffer base - abi::emit_symbol_address(emitter, "r9", "_ftp_tcp_prefix"); // \"tcp://\" prefix base emitter.instruction("xor rcx, rcx"); // address write index + emitter.instruction("mov r9, QWORD PTR [rbp - 8]"); // hostname pointer + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // hostname byte length + emitter.instruction("xor rdx, rdx"); // scheme scan index + emitter.label("__rt_fsockopen_scheme_x86"); + emitter.instruction("lea rax, [rdx + 2]"); // index the second '/' would occupy + emitter.instruction("cmp rax, r10"); // does a "://" marker still fit? + emitter.instruction("jge __rt_fsockopen_no_scheme_x86"); // no transport prefix - php defaults to tcp + emitter.instruction("cmp BYTE PTR [r9 + rdx], 58"); // is the candidate byte ':'? + emitter.instruction("jne __rt_fsockopen_scheme_next_x86"); // not the marker - keep scanning + emitter.instruction("cmp BYTE PTR [r9 + rdx + 1], 47"); // is the next byte '/'? + emitter.instruction("jne __rt_fsockopen_scheme_next_x86"); // not the marker - keep scanning + emitter.instruction("cmp BYTE PTR [r9 + rdx + 2], 47"); // is the byte after that '/'? + emitter.instruction("je __rt_fsockopen_pfx_done_x86"); // hostname has its own transport - copy it verbatim + emitter.label("__rt_fsockopen_scheme_next_x86"); + emitter.instruction("inc rdx"); // advance the scheme scan index + emitter.instruction("jmp __rt_fsockopen_scheme_x86"); // keep looking for "://" + emitter.label("__rt_fsockopen_no_scheme_x86"); + abi::emit_symbol_address(emitter, "r9", "_ftp_tcp_prefix"); // \"tcp://\" prefix base emitter.label("__rt_fsockopen_pfx_x86"); emitter.instruction("cmp rcx, 6"); // copied the whole \"tcp://\" prefix? emitter.instruction("jge __rt_fsockopen_pfx_done_x86"); // prefix copied diff --git a/src/codegen_support/runtime/io/ftp.rs b/src/codegen_support/runtime/io/ftp.rs index 901dfac3f3..1ccd2920e7 100644 --- a/src/codegen_support/runtime/io/ftp.rs +++ b/src/codegen_support/runtime/io/ftp.rs @@ -45,31 +45,29 @@ pub fn emit_ftp(emitter: &mut Emitter) { emitter.instruction("str x0, [sp, #0]"); // save the control descriptor // -- write phase: TLS-aware dispatch -- - abi::emit_symbol_address(emitter, "x13", "_tls_sessions"); - emitter.instruction("ldr x14, [x13, x0, lsl #3]"); // _tls_sessions[fd] handle (0 = plain TCP) - emitter.instruction("cbz x14, __rt_ftp_sr_plain_write"); // no TLS → plain write syscall - emitter.instruction("mov x0, x14"); // TLS handle as first arg (x1/x2 still hold cmd ptr/len) + emitter.instruction("bl __rt_tls_session_get"); // resolve the full-width descriptor through the bounded TLS map + emitter.instruction("cbz x0, __rt_ftp_sr_plain_write"); // no TLS → plain write syscall abi::emit_symbol_address(emitter, "x9", "_elephc_tls_write_fn"); emitter.instruction("ldr x9, [x9]"); // load the elephc_tls_write entry pointer - emitter.instruction("blr x9"); // send the command through TLS + emitter.emit_published_bridge_call("x9"); // send the command through the published TLS entry emitter.instruction("b __rt_ftp_sr_read_phase"); // continue at target label emitter.label("__rt_ftp_sr_plain_write"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the control descriptor after the TLS lookup miss emitter.syscall(4); // plain write syscall // -- read phase: TLS-aware dispatch into _ftp_resp_buf -- emitter.label("__rt_ftp_sr_read_phase"); emitter.instruction("ldr x0, [sp, #0]"); // reload control fd - abi::emit_symbol_address(emitter, "x13", "_tls_sessions"); - emitter.instruction("ldr x14, [x13, x0, lsl #3]"); // load runtime value - emitter.instruction("cbz x14, __rt_ftp_sr_plain_read"); // no TLS → plain read syscall - emitter.instruction("mov x0, x14"); // TLS handle as first arg + emitter.instruction("bl __rt_tls_session_get"); // resolve the full-width descriptor through the bounded TLS map + emitter.instruction("cbz x0, __rt_ftp_sr_plain_read"); // no TLS → plain read syscall abi::emit_symbol_address(emitter, "x1", "_ftp_resp_buf"); emitter.instruction("mov x2, #4096"); // response buffer capacity abi::emit_symbol_address(emitter, "x9", "_elephc_tls_read_fn"); emitter.instruction("ldr x9, [x9]"); // load the elephc_tls_read entry pointer - emitter.instruction("blr x9"); // read the reply through TLS + emitter.emit_published_bridge_call("x9"); // read the reply through the published TLS entry emitter.instruction("b __rt_ftp_sr_done"); // continue at target label emitter.label("__rt_ftp_sr_plain_read"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the control descriptor after the TLS lookup miss abi::emit_symbol_address(emitter, "x1", "_ftp_resp_buf"); emitter.instruction("mov x2, #4096"); // response buffer capacity emitter.syscall(3); // plain read syscall @@ -236,12 +234,13 @@ pub fn emit_ftp(emitter: &mut Emitter) { abi::emit_symbol_address(emitter, "x9", "_elephc_tls_attach_fd_fn"); emitter.instruction("ldr x9, [x9]"); // load runtime value emitter.instruction("cbz x9, __rt_ftp_open_fail"); // missing elephc-tls runtime means FTPS open fails closed - emitter.instruction("blr x9"); // x0 = TLS handle or -1 + emitter.emit_published_bridge_call("x9"); // x0 = TLS handle or -1 through the published ABI entry emitter.instruction("cmp x0, #0"); // compare runtime values for the next branch emitter.instruction("b.lt __rt_ftp_open_fail"); // TLS handshake failed - emitter.instruction("ldr x10, [sp, #0]"); // fd - abi::emit_symbol_address(emitter, "x11", "_tls_sessions"); - emitter.instruction("str x0, [x11, x10, lsl #3]"); // _tls_sessions[fd] = handle + emitter.instruction("mov x1, x0"); // pass the new TLS session as the table value + emitter.instruction("ldr x0, [sp, #0]"); // pass the full-width control descriptor as the table key + emitter.instruction("bl __rt_tls_session_set"); // associate the control socket without raw indexing + emitter.instruction("cbz x0, __rt_ftp_open_fail"); // bounded table exhaustion fails the FTPS upgrade emitter.label("__rt_ftp_open_skip_auth_tls"); emitter.instruction("ldr x0, [sp, #0]"); // control descriptor for the USER command @@ -296,12 +295,13 @@ pub fn emit_ftp(emitter: &mut Emitter) { abi::emit_symbol_address(emitter, "x9", "_elephc_tls_attach_fd_fn"); emitter.instruction("ldr x9, [x9]"); // load runtime value emitter.instruction("cbz x9, __rt_ftp_open_fail"); // missing elephc-tls runtime means FTPS open fails closed - emitter.instruction("blr x9"); // x0 = TLS handle or -1 + emitter.emit_published_bridge_call("x9"); // x0 = TLS handle or -1 through the published ABI entry emitter.instruction("cmp x0, #0"); // compare runtime values for the next branch emitter.instruction("b.lt __rt_ftp_open_fail"); // data-channel TLS handshake failed - emitter.instruction("ldr x10, [sp, #24]"); // data fd - abi::emit_symbol_address(emitter, "x11", "_tls_sessions"); - emitter.instruction("str x0, [x11, x10, lsl #3]"); // _tls_sessions[data_fd] = handle + emitter.instruction("mov x1, x0"); // pass the new data-channel TLS session as the table value + emitter.instruction("ldr x0, [sp, #24]"); // pass the full-width data descriptor as the table key + emitter.instruction("bl __rt_tls_session_set"); // associate the data socket without raw indexing + emitter.instruction("cbz x0, __rt_ftp_open_fail"); // bounded table exhaustion fails the FTPS upgrade emitter.label("__rt_ftp_open_skip_data_tls"); // -- optional REST \r\n send when stream_context_options['ftp']['resume_pos'] is set -- @@ -379,29 +379,32 @@ fn emit_ftp_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save command length (call write may clobber rdx) // -- write phase: TLS-aware dispatch -- - abi::emit_symbol_address(emitter, "r10", "_tls_sessions"); // load runtime data address - emitter.instruction("mov r11, QWORD PTR [r10 + rdi * 8]"); // _tls_sessions[fd] handle (0 = plain TCP) - emitter.instruction("test r11, r11"); // check whether the runtime value is zero + emitter.instruction("call __rt_tls_session_get"); // resolve the full-width descriptor through the bounded TLS map + emitter.instruction("test rax, rax"); // check whether the runtime value is zero emitter.instruction("jz __rt_ftp_sr_plain_write_x"); // branch when the checked value is zero or equal - emitter.instruction("mov rdi, r11"); // TLS handle as first arg + emitter.instruction("mov rdi, rax"); // TLS handle as first arg + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload the command pointer after the lookup helper + emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // reload the command length after the lookup helper abi::emit_load_symbol_to_reg(emitter, "r9", "_elephc_tls_write_fn", 0); // prepare SysV call argument - emitter.instruction("call r9"); // send the command through TLS + emitter.emit_published_bridge_call("r9"); // send the command through the published TLS ABI entry emitter.instruction("jmp __rt_ftp_sr_read_phase_x"); // continue at target label emitter.label("__rt_ftp_sr_plain_write_x"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the control descriptor after the TLS lookup miss + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload the plain command pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // reload the plain command length emitter.instruction("call write"); // send the command on the control socket // -- read phase: TLS-aware dispatch into _ftp_resp_buf -- emitter.label("__rt_ftp_sr_read_phase_x"); emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload control fd - abi::emit_symbol_address(emitter, "r10", "_tls_sessions"); // load runtime data address - emitter.instruction("mov r11, QWORD PTR [r10 + rdi * 8]"); // _tls_sessions[fd] handle - emitter.instruction("test r11, r11"); // check whether the runtime value is zero + emitter.instruction("call __rt_tls_session_get"); // resolve the full-width descriptor through the bounded TLS map + emitter.instruction("test rax, rax"); // check whether the runtime value is zero emitter.instruction("jz __rt_ftp_sr_plain_read_x"); // branch when the checked value is zero or equal - emitter.instruction("mov rdi, r11"); // TLS handle + emitter.instruction("mov rdi, rax"); // TLS handle abi::emit_symbol_address(emitter, "rsi", "_ftp_resp_buf"); // load runtime data address emitter.instruction("mov rdx, 4096"); // prepare SysV call argument abi::emit_load_symbol_to_reg(emitter, "r9", "_elephc_tls_read_fn", 0); // prepare SysV call argument - emitter.instruction("call r9"); // read the reply through TLS + emitter.emit_published_bridge_call("r9"); // read the reply through the published TLS ABI entry emitter.instruction("jmp __rt_ftp_sr_done_x"); // continue at target label emitter.label("__rt_ftp_sr_plain_read_x"); abi::emit_symbol_address(emitter, "rsi", "_ftp_resp_buf"); // response buffer pointer @@ -561,12 +564,14 @@ fn emit_ftp_linux_x86_64(emitter: &mut Emitter) { abi::emit_load_symbol_to_reg(emitter, "r9", "_elephc_tls_attach_fd_fn", 0); // prepare SysV call argument emitter.instruction("test r9, r9"); // missing elephc-tls runtime means FTPS open fails closed emitter.instruction("jz __rt_ftp_open_fail_x86"); // return a failed stream when no TLS entry is available - emitter.instruction("call r9"); // rax = TLS handle or -1 + emitter.emit_published_bridge_call("r9"); // rax = TLS handle or -1 through the published ABI entry emitter.instruction("cmp rax, 0"); // compare runtime values for the next branch emitter.instruction("jl __rt_ftp_open_fail_x86"); // TLS handshake failed - emitter.instruction("mov rcx, QWORD PTR [rbp - 8]"); // fd - abi::emit_symbol_address(emitter, "r10", "_tls_sessions"); // load runtime data address - emitter.instruction("mov QWORD PTR [r10 + rcx * 8], rax"); // _tls_sessions[fd] = handle + emitter.instruction("mov rsi, rax"); // pass the new TLS session as the table value + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // pass the full-width control descriptor as the table key + emitter.instruction("call __rt_tls_session_set"); // associate the control socket without raw indexing + emitter.instruction("test rax, rax"); // did the bounded table accept the association? + emitter.instruction("jz __rt_ftp_open_fail_x86"); // table exhaustion fails the FTPS upgrade emitter.label("__rt_ftp_open_skip_auth_tls_x"); emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // control descriptor for the USER command @@ -621,12 +626,14 @@ fn emit_ftp_linux_x86_64(emitter: &mut Emitter) { abi::emit_load_symbol_to_reg(emitter, "r9", "_elephc_tls_attach_fd_fn", 0); // prepare SysV call argument emitter.instruction("test r9, r9"); // missing elephc-tls runtime means FTPS open fails closed emitter.instruction("jz __rt_ftp_open_fail_x86"); // return a failed stream when no TLS entry is available - emitter.instruction("call r9"); // rax = TLS handle or -1 + emitter.emit_published_bridge_call("r9"); // rax = TLS handle or -1 through the published ABI entry emitter.instruction("cmp rax, 0"); // compare runtime values for the next branch emitter.instruction("jl __rt_ftp_open_fail_x86"); // data-channel TLS handshake failed - emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // data fd - abi::emit_symbol_address(emitter, "r10", "_tls_sessions"); // load runtime data address - emitter.instruction("mov QWORD PTR [r10 + rcx * 8], rax"); // _tls_sessions[data_fd] = handle + emitter.instruction("mov rsi, rax"); // pass the new data-channel TLS session as the table value + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // pass the full-width data descriptor as the table key + emitter.instruction("call __rt_tls_session_set"); // associate the data socket without raw indexing + emitter.instruction("test rax, rax"); // did the bounded table accept the association? + emitter.instruction("jz __rt_ftp_open_fail_x86"); // table exhaustion fails the FTPS upgrade emitter.label("__rt_ftp_open_skip_data_tls_x"); // -- optional REST \r\n send when stream_context_options['ftp']['resume_pos'] is set -- diff --git a/src/codegen_support/runtime/io/fwrite.rs b/src/codegen_support/runtime/io/fwrite.rs index fd585765e2..dd395aa447 100644 --- a/src/codegen_support/runtime/io/fwrite.rs +++ b/src/codegen_support/runtime/io/fwrite.rs @@ -13,7 +13,7 @@ //! - A payload larger than the 64 KiB scratch is written unfiltered; v1 stream //! filters target the common small-write case. -use crate::codegen_support::{abi, emit::Emitter, platform::Arch}; +use crate::codegen_support::{abi, emit::Emitter, platform::{Arch, Platform}}; const FILTER_BUF_SIZE: i64 = 65536; @@ -146,15 +146,24 @@ pub fn emit_fwrite(emitter: &mut Emitter) { emitter.instruction("ldr x2, [sp, #16]"); // payload length // -- TLS dispatch: route through elephc_tls_write when fd has an // attached session (Phase 11 B3). -- - abi::emit_symbol_address(emitter, "x13", "_tls_sessions"); - emitter.instruction("ldr x14, [x13, x0, lsl #3]"); // _tls_sessions[fd] handle (0 = plain TCP) - emitter.instruction("cbz x14, __rt_fwrite_syscall"); // no TLS attached → write syscall - emitter.instruction("mov x0, x14"); // handle as first arg + emitter.instruction("bl __rt_tls_session_get"); // resolve the full-width descriptor through the bounded TLS map + emitter.instruction("cbz x0, __rt_fwrite_syscall"); // no TLS attached → write syscall + emitter.instruction("ldr x1, [sp, #8]"); // reload the payload pointer after the lookup helper + emitter.instruction("ldr x2, [sp, #16]"); // reload the payload length after the lookup helper abi::emit_symbol_address(emitter, "x9", "_elephc_tls_write_fn"); emitter.instruction("ldr x9, [x9]"); // load runtime value - emitter.instruction("blr x9"); // x0 = bytes written or -1 + emitter.emit_published_bridge_call("x9"); // x0 = bytes written or one documented TLS v2 sentinel + emitter.instruction("cmp x0, #-2"); // WouldBlock uses an internal retry sentinel, not PHP's write result + emitter.instruction("b.eq __rt_fwrite_tls_retry"); // normalize a retryable TLS write to fwrite's existing -1 result + emitter.instruction("cmp x0, #-3"); // TimedOut is also a retryable TLS condition + emitter.instruction("b.ne __rt_fwrite_return"); // completed writes, EOF-equivalent errors, and terminal errors keep their result + emitter.label("__rt_fwrite_tls_retry"); + emitter.instruction("mov x0, #-1"); // preserve fwrite's historical PHP-visible error return emitter.instruction("b __rt_fwrite_return"); // continue at target label emitter.label("__rt_fwrite_syscall"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the plain descriptor after the TLS lookup miss + emitter.instruction("ldr x1, [sp, #8]"); // reload the plain-write payload pointer + emitter.instruction("ldr x2, [sp, #16]"); // reload the plain-write payload length emitter.syscall(4); emitter.label("__rt_fwrite_return"); emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address @@ -191,8 +200,19 @@ fn emit_fwrite_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the payload length // -- look up the write filter for this descriptor -- + if emitter.target.platform == Platform::Windows { + emitter.instruction("call __rt_win_stream_slot"); // map opaque Windows descriptor to bounded filter-table slot + emitter.instruction("mov r10, rax"); // preserve compact slot for table lookup + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // restore the real descriptor for later write/filter dispatch + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // restore the payload pointer clobbered by the slot lookup + emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // restore the payload length clobbered by the slot lookup + } abi::emit_symbol_address(emitter, "r9", "_stream_write_filters"); // write-filter table base - emitter.instruction("movzx ecx, BYTE PTR [r9 + rdi]"); // write filter id for this descriptor + if emitter.target.platform == Platform::Windows { + emitter.instruction("movzx ecx, BYTE PTR [r9 + r10]"); // write filter id for compact stream slot + } else { + emitter.instruction("movzx ecx, BYTE PTR [r9 + rdi]"); // write filter id for this descriptor + } emitter.instruction("test rcx, rcx"); // is a write filter attached? emitter.instruction("jz __rt_fwrite_direct_x86"); // no filter: write the payload directly emitter.instruction("cmp rcx, 128"); // user-filter id range (>= USER_FILTER_ID_BASE)? @@ -270,22 +290,101 @@ fn emit_fwrite_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jmp __rt_fwrite_direct_x86"); // fall through to the standard direct-write path emitter.label("__rt_fwrite_direct_x86"); + if emitter.target.platform == Platform::Windows { + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // restore the opaque descriptor before starting a new write operation + emitter.instruction("call __rt_win_stream_clear_timed_out"); // every write begins with timed_out=false, including TLS-dispatched writes + } emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // file descriptor emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // payload pointer (original or filtered) emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // payload length // -- TLS dispatch (Phase 11 B3) -- - abi::emit_symbol_address(emitter, "r10", "_tls_sessions"); // load runtime data address - emitter.instruction("mov r11, QWORD PTR [r10 + rdi * 8]"); // _tls_sessions[fd] handle - emitter.instruction("test r11, r11"); // check whether the runtime value is zero + emitter.instruction("call __rt_tls_session_get"); // resolve the full-width descriptor through the bounded TLS map + emitter.instruction("test rax, rax"); // check whether the runtime value is zero emitter.instruction("jz __rt_fwrite_syscall_x86"); // plain TCP → libc write - emitter.instruction("mov rdi, r11"); // handle as first arg + emitter.instruction("mov rdi, rax"); // handle as first arg + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload the payload pointer after the lookup helper + emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // reload the payload length after the lookup helper abi::emit_load_symbol_to_reg(emitter, "r9", "_elephc_tls_write_fn", 0); // prepare SysV call argument - emitter.instruction("call r9"); // rax = bytes written or -1 - emitter.instruction("jmp __rt_fwrite_return_x86"); // continue at target label + emitter.emit_published_bridge_call("r9"); // rax = bytes written or one documented TLS v2 sentinel + emitter.instruction("cmp rax, -3"); // TLS v2 TimedOut requires PHP-visible Windows stream metadata + if emitter.target.platform == Platform::Windows { + emitter.instruction("jne __rt_fwrite_tls_not_timed_out_x86"); // only TimedOut updates the stream timeout flag + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // restore the opaque stream descriptor for timeout metadata + emitter.instruction("call __rt_win_stream_mark_timed_out"); // record TimedOut without any EOF mutation + emitter.instruction("mov rax, -1"); // preserve fwrite's historical PHP-visible error return + emitter.instruction("jmp __rt_fwrite_return_x86"); // return the normalized error result + emitter.label("__rt_fwrite_tls_not_timed_out_x86"); + } else { + emitter.instruction("je __rt_fwrite_tls_retry_x86"); // normalize TimedOut to fwrite's established error result + } + emitter.instruction("cmp rax, -2"); // TLS v2 WouldBlock is internal to the bridge ABI + emitter.instruction("jne __rt_fwrite_return_x86"); // completed writes and terminal errors keep their existing result + emitter.label("__rt_fwrite_tls_retry_x86"); + emitter.instruction("mov rax, -1"); // preserve fwrite's historical PHP-visible error return + emitter.instruction("jmp __rt_fwrite_return_x86"); // return the normalized retryable write failure emitter.label("__rt_fwrite_syscall_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the plain descriptor after the TLS lookup miss + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload the plain-write payload pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // reload the plain-write payload length emitter.instruction("call write"); // write the payload through libc write() + if emitter.target.platform == Platform::Windows { + emitter.instruction("test rax, rax"); // only failed writes can carry ETIMEDOUT + emitter.instruction("jns __rt_fwrite_return_x86"); // successful writes leave timed_out cleared + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // preserve the write failure sentinel across errno lookup + emitter.instruction("call __errno_location"); // inspect the POSIX-mapped Winsock error + emitter.instruction("cmp DWORD PTR [rax], 110"); // ETIMEDOUT is observable but must not imply EOF + emitter.instruction("jne __rt_fwrite_restore_error_x86"); // unrelated write errors keep their existing return behavior + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // restore the opaque stream descriptor for timeout metadata + emitter.instruction("call __rt_win_stream_mark_timed_out"); // record timed_out without any EOF mutation + emitter.label("__rt_fwrite_restore_error_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // restore the original write failure sentinel + } emitter.label("__rt_fwrite_return_x86"); emitter.instruction("add rsp, 32"); // release the frame emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the byte count from write } + +#[cfg(test)] +mod tests { + use super::*; + use crate::codegen::platform::Target; + + /// Verifies TLS v2 retry sentinels are normalized to fwrite's established + /// error result and that Windows records the TimedOut sentinel separately. + #[test] + fn tls_retry_sentinels_preserve_fwrite_result_and_windows_timeout_state() { + let mut linux = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_fwrite(&mut linux); + let linux_asm = linux.output(); + assert!(linux_asm.contains("cmp rax, -3")); + assert!(linux_asm.contains("je __rt_fwrite_tls_retry_x86")); + assert!(linux_asm.contains("cmp rax, -2")); + + let mut windows = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_fwrite(&mut windows); + let windows_asm = windows.output(); + assert!(windows_asm.contains("call __rt_win_stream_mark_timed_out")); + assert!(windows_asm.contains("mov rax, -1")); + + let mut arm = Emitter::new(Target::new(Platform::MacOS, Arch::AArch64)); + emit_fwrite(&mut arm); + let arm_asm = arm.output(); + assert!(arm_asm.contains("cmp x0, #-2")); + assert!(arm_asm.contains("cmp x0, #-3")); + assert!(arm_asm.contains("mov x0, #-1")); + } + + /// Verifies Windows restores the payload arguments after resolving the + /// bounded stream slot so write filters see the caller's actual bytes. + #[test] + fn windows_filter_slot_lookup_preserves_fwrite_payload() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_fwrite(&mut emitter); + let asm = emitter.output(); + let slot_call = asm.find("call __rt_win_stream_slot").expect("missing slot lookup"); + let filtered_path = &asm[slot_call..]; + assert!(filtered_path.contains("mov rsi, QWORD PTR [rbp - 16]")); + assert!(filtered_path.contains("mov rdx, QWORD PTR [rbp - 24]")); + } +} diff --git a/src/codegen_support/runtime/io/gethostbyaddr.rs b/src/codegen_support/runtime/io/gethostbyaddr.rs index d0640e60a8..df4bcbfd52 100644 --- a/src/codegen_support/runtime/io/gethostbyaddr.rs +++ b/src/codegen_support/runtime/io/gethostbyaddr.rs @@ -112,7 +112,7 @@ fn emit_gethostbyaddr_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("lea rdi, [rbp - 24]"); // pointer to the in_addr emitter.instruction("mov rsi, 4"); // in_addr length in bytes emitter.instruction(&format!("mov rdx, {}", AF_INET)); // address family AF_INET - emitter.instruction("call gethostbyaddr"); // rax = struct hostent* (null when no record) + emitter.emit_call_c("gethostbyaddr"); // rax = struct hostent* (null when no record) emitter.instruction("test rax, rax"); // did the reverse lookup find a record? emitter.instruction("jz __rt_gethostbyaddr_input_x86"); // no record: return the address unchanged emitter.instruction("mov rax, QWORD PTR [rax]"); // hostent.h_name: NUL-terminated C string diff --git a/src/codegen_support/runtime/io/getprotobyname.rs b/src/codegen_support/runtime/io/getprotobyname.rs index 43a79871f9..1fa95e74be 100644 --- a/src/codegen_support/runtime/io/getprotobyname.rs +++ b/src/codegen_support/runtime/io/getprotobyname.rs @@ -11,13 +11,17 @@ //! - Returns the protocol number, or -1 when no entry matches; the builtin //! emitter boxes -1 as PHP `false`. -use crate::codegen_support::{emit::Emitter, platform::Arch}; +use crate::codegen_support::{emit::Emitter, platform::{Arch, Platform}}; /// getprotobyname: look up a protocol number by name. /// Input: AArch64 x0 = query pointer, x1 = query length /// x86_64 rdi = query pointer, rsi = query length /// Output: protocol number, or -1 when no entry matches pub fn emit_getprotobyname(emitter: &mut Emitter) { + if emitter.platform == Platform::Windows { + emit_getprotobyname_windows_x86_64(emitter); + return; + } if emitter.target.arch == Arch::X86_64 { emit_getprotobyname_linux_x86_64(emitter); return; @@ -187,6 +191,32 @@ pub fn emit_getprotobyname(emitter: &mut Emitter) { emitter.instruction("ret"); // return the protocol number or -1 } +/// Emits the Windows x86_64 protocol lookup through Winsock `getprotobyname`. +/// +/// The compiler string is converted to a bounded C scratch string before the +/// Winsock call; `protoent.p_proto` is a signed 16-bit integer at offset 16. +fn emit_getprotobyname_windows_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: getprotobyname ---"); + emitter.label_global("__rt_getprotobyname"); + emitter.instruction("sub rsp, 8"); // align nested SysV and MSx64-shim calls from the function-entry stack + emitter.instruction("mov rax, rdi"); // move the counted elephc name into the C-string conversion input + emitter.instruction("mov rdx, rsi"); // move the counted elephc name length into the conversion input + emitter.instruction("call __rt_cstr"); // copy and NUL-terminate the name in bounded scratch storage + emitter.instruction("mov rdi, rax"); // pass the C string to the Winsock ABI shim + emitter.instruction("call __rt_sys_getprotobyname"); // return a transient protoent pointer or null + emitter.instruction("test rax, rax"); // did Winsock find a protocol entry? + emitter.instruction("jz __rt_gpbn_windows_missing"); // return PHP's -1 missing sentinel on lookup failure + emitter.instruction("movsx rax, WORD PTR [rax + 16]"); // load MinGW/Winsock's signed short protoent.p_proto + emitter.instruction("add rsp, 8"); // release the alignment slot before returning to the builtin lowering + emitter.instruction("ret"); // return the protocol number to the builtin lowering + emitter.label("__rt_gpbn_windows_missing"); + emitter.instruction("mov rax, -1"); // use the existing PHP false sentinel for a missing entry + emitter.instruction("add rsp, 8"); // release the alignment slot before returning the failure sentinel + emitter.instruction("ret"); // return the lookup failure sentinel + emitter.blank(); +} + /// Emits the Linux x86_64 stream runtime helper for getprotobyname. fn emit_getprotobyname_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); diff --git a/src/codegen_support/runtime/io/getprotobynumber.rs b/src/codegen_support/runtime/io/getprotobynumber.rs index 60d9eb9b4f..8d46e19dbf 100644 --- a/src/codegen_support/runtime/io/getprotobynumber.rs +++ b/src/codegen_support/runtime/io/getprotobynumber.rs @@ -11,13 +11,17 @@ //! - Returns a null pointer when no entry matches; the builtin emitter boxes //! that as PHP `false`. -use crate::codegen_support::{emit::Emitter, platform::Arch}; +use crate::codegen_support::{emit::Emitter, platform::{Arch, Platform}}; /// getprotobynumber: look up a protocol name by number. /// Input: AArch64 x0 = protocol number /// x86_64 rdi = protocol number /// Output: string pointer/length, or a null pointer when no entry matches pub fn emit_getprotobynumber(emitter: &mut Emitter) { + if emitter.platform == Platform::Windows { + emit_getprotobynumber_windows_x86_64(emitter); + return; + } if emitter.target.arch == Arch::X86_64 { emit_getprotobynumber_linux_x86_64(emitter); return; @@ -157,6 +161,39 @@ pub fn emit_getprotobynumber(emitter: &mut Emitter) { emitter.instruction("ret"); // return the not-found result } +/// Emits the Windows x86_64 protocol-name lookup through Winsock. +/// +/// `protoent.p_name` is a transient C string, so its byte span is measured and +/// copied with `__rt_str_persist` before another Winsock lookup can overwrite it. +fn emit_getprotobynumber_windows_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: getprotobynumber ---"); + emitter.label_global("__rt_getprotobynumber"); + emitter.instruction("sub rsp, 8"); // align nested SysV and MSx64-shim calls from the function-entry stack + emitter.instruction("call __rt_sys_getprotobynumber"); // return a transient protoent pointer or null + emitter.instruction("test rax, rax"); // did Winsock find a protocol entry? + emitter.instruction("jz __rt_gpbnum_windows_missing"); // return PHP false when no entry exists + emitter.instruction("mov rax, QWORD PTR [rax]"); // load protoent.p_name at the x64 structure head + emitter.instruction("test rax, rax"); // guard a malformed entry without a canonical name + emitter.instruction("jz __rt_gpbnum_windows_missing"); // treat an absent canonical name as PHP false + emitter.instruction("xor edx, edx"); // initialize the C-string byte length accumulator + emitter.label("__rt_gpbnum_windows_strlen"); + emitter.instruction("cmp BYTE PTR [rax + rdx], 0"); // stop at the transient C-string terminator + emitter.instruction("je __rt_gpbnum_windows_persist"); // persist once the complete byte span is known + emitter.instruction("inc rdx"); // count one canonical-name byte + emitter.instruction("jmp __rt_gpbnum_windows_strlen"); // continue measuring the transient name + emitter.label("__rt_gpbnum_windows_persist"); + emitter.instruction("call __rt_str_persist"); // copy the result into owned storage before another lookup + emitter.instruction("add rsp, 8"); // release the alignment slot before returning the owned string + emitter.instruction("ret"); // return the owned PHP string pair + emitter.label("__rt_gpbnum_windows_missing"); + emitter.instruction("xor eax, eax"); // return a null string pointer for PHP false + emitter.instruction("xor edx, edx"); // return no string length on lookup failure + emitter.instruction("add rsp, 8"); // release the alignment slot before returning the missing result + emitter.instruction("ret"); // return the missing-entry result pair + emitter.blank(); +} + /// Emits the Linux x86_64 stream runtime helper for getprotobynumber. fn emit_getprotobynumber_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); diff --git a/src/codegen_support/runtime/io/getservbyname.rs b/src/codegen_support/runtime/io/getservbyname.rs index 0c4fc74a0c..a067e48a4b 100644 --- a/src/codegen_support/runtime/io/getservbyname.rs +++ b/src/codegen_support/runtime/io/getservbyname.rs @@ -12,13 +12,17 @@ //! - Returns the port number, or -1 when no entry matches; the builtin emitter //! boxes -1 as PHP `false`. -use crate::codegen_support::{emit::Emitter, platform::Arch}; +use crate::codegen_support::{emit::Emitter, platform::{Arch, Platform}}; /// getservbyname: look up a service port by name and protocol. /// Input: AArch64 x1/x2 = service name, x3/x4 = protocol name /// x86_64 rdi/rsi = service name, rdx/rcx = protocol name /// Output: port number, or -1 when no entry matches pub fn emit_getservbyname(emitter: &mut Emitter) { + if emitter.platform == Platform::Windows { + emit_getservbyname_windows_x86_64(emitter); + return; + } if emitter.target.arch == Arch::X86_64 { emit_getservbyname_linux_x86_64(emitter); return; @@ -269,6 +273,49 @@ pub fn emit_getservbyname(emitter: &mut Emitter) { emitter.instruction("ret"); // return the port number or -1 } +/// Emits the Windows x86_64 service lookup through Winsock `getservbyname`. +/// +/// Each counted PHP string gets its own bounded C scratch buffer so converting +/// the protocol cannot overwrite the service name before the native call. +fn emit_getservbyname_windows_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: getservbyname ---"); + emitter.label_global("__rt_getservbyname"); + emitter.instruction("push rbp"); // preserve the caller frame pointer for the conversion spill frame + emitter.instruction("mov rbp, rsp"); // establish a stable base for all four input components + emitter.instruction("sub rsp, 32"); // reserve service and protocol pointer-length spill slots + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the counted service pointer across C-string conversion + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the counted service length across C-string conversion + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the counted protocol pointer across C-string conversion + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the counted protocol length across C-string conversion + emitter.instruction("test rcx, rcx"); // did PHP pass the empty protocol string that Winsock treats as NULL? + emitter.instruction("jz __rt_gsbn_windows_missing"); // PHP requires false instead of Winsock's protocol-agnostic lookup + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // load the service payload into the primary C-string input + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // load the service byte length into the primary C-string input + emitter.instruction("call __rt_cstr"); // NUL-terminate the service in primary bounded scratch storage + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // retain the primary C-string pointer for the Winsock call + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // load the protocol payload into the secondary C-string input + emitter.instruction("mov rdx, QWORD PTR [rbp - 32]"); // load the protocol byte length into the secondary C-string input + emitter.instruction("call __rt_cstr2"); // NUL-terminate the protocol without clobbering the service scratch buffer + emitter.instruction("mov rsi, rax"); // pass the protocol C string in the shim's second SysV argument + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // pass the preserved service C string in the shim's first SysV argument + emitter.instruction("call __rt_sys_getservbyname"); // return a transient servent pointer or null + emitter.instruction("test rax, rax"); // did Winsock find a matching service entry? + emitter.instruction("jz __rt_gsbn_windows_missing"); // map a missing service to PHP false + emitter.instruction("movzx eax, WORD PTR [rax + 24]"); // load MinGW/Winsock servent.s_port in network byte order + emitter.instruction("xchg al, ah"); // convert the 16-bit service port to PHP's host-order integer + emitter.instruction("movzx eax, ax"); // clear the high bits after the byte swap + emitter.instruction("add rsp, 32"); // release the conversion spill frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the host-order service port + emitter.label("__rt_gsbn_windows_missing"); + emitter.instruction("mov rax, -1"); // use the existing PHP false sentinel for lookup failure + emitter.instruction("add rsp, 32"); // release the conversion spill frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the missing-service sentinel + emitter.blank(); +} + /// Emits the Linux x86_64 stream runtime helper for getservbyname. fn emit_getservbyname_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); diff --git a/src/codegen_support/runtime/io/getservbyport.rs b/src/codegen_support/runtime/io/getservbyport.rs index faf7259b33..7fab3f1a6e 100644 --- a/src/codegen_support/runtime/io/getservbyport.rs +++ b/src/codegen_support/runtime/io/getservbyport.rs @@ -12,13 +12,17 @@ //! - On a match the canonical name is duplicated into owned heap storage via //! `__rt_str_persist`; a null pointer is returned when no entry matches. -use crate::codegen_support::{emit::Emitter, platform::Arch}; +use crate::codegen_support::{emit::Emitter, platform::{Arch, Platform}}; /// getservbyport: look up a service name by port number and protocol. /// Input: AArch64 x0 = port, x1/x2 = protocol name /// x86_64 rdi = port, rsi/rdx = protocol name /// Output: string pointer/length, or a null pointer when no entry matches pub fn emit_getservbyport(emitter: &mut Emitter) { + if emitter.platform == Platform::Windows { + emit_getservbyport_windows_x86_64(emitter); + return; + } if emitter.target.arch == Arch::X86_64 { emit_getservbyport_linux_x86_64(emitter); return; @@ -199,6 +203,51 @@ pub fn emit_getservbyport(emitter: &mut Emitter) { emitter.instruction("ret"); // return the not-found result } +/// Emits the Windows x86_64 service-name lookup through Winsock. +/// +/// `servent.s_name` is transient native storage. The helper measures and +/// persists it before returning so subsequent Winsock calls cannot invalidate +/// the PHP string result. +fn emit_getservbyport_windows_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: getservbyport ---"); + emitter.label_global("__rt_getservbyport"); + emitter.instruction("push rbp"); // preserve the caller frame pointer for the port spill frame + emitter.instruction("mov rbp, rsp"); // establish a stable base across C-string conversion + emitter.instruction("sub rsp, 16"); // reserve the host-order port and protocol-length spill slots + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the PHP host-order port across protocol conversion + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the counted protocol length across conversion + emitter.instruction("mov rax, rsi"); // load the protocol payload into the C-string conversion input + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload the protocol byte length into the conversion input + emitter.instruction("call __rt_cstr"); // NUL-terminate the protocol in bounded scratch storage + emitter.instruction("mov rsi, rax"); // pass the protocol C string as the shim's second SysV argument + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // restore the PHP host-order port as the shim's first SysV argument + emitter.instruction("call __rt_sys_getservbyport"); // return a transient servent pointer or null + emitter.instruction("test rax, rax"); // did Winsock find a matching service entry? + emitter.instruction("jz __rt_gsbp_windows_missing"); // map a missing service to PHP false + emitter.instruction("mov rax, QWORD PTR [rax]"); // load servent.s_name from the x64 structure head + emitter.instruction("test rax, rax"); // guard a malformed entry without a canonical name + emitter.instruction("jz __rt_gsbp_windows_missing"); // treat an absent canonical name as PHP false + emitter.instruction("xor edx, edx"); // initialize the C-string byte length accumulator + emitter.label("__rt_gsbp_windows_strlen"); + emitter.instruction("cmp BYTE PTR [rax + rdx], 0"); // stop at the transient C-string terminator + emitter.instruction("je __rt_gsbp_windows_persist"); // persist after measuring the complete canonical name + emitter.instruction("inc rdx"); // count one canonical-name byte + emitter.instruction("jmp __rt_gsbp_windows_strlen"); // continue measuring the transient name + emitter.label("__rt_gsbp_windows_persist"); + emitter.instruction("call __rt_str_persist"); // copy the result into owned storage before another lookup + emitter.instruction("add rsp, 16"); // release the conversion spill frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the owned PHP string pair + emitter.label("__rt_gsbp_windows_missing"); + emitter.instruction("xor eax, eax"); // return a null string pointer for PHP false + emitter.instruction("xor edx, edx"); // return no string length on lookup failure + emitter.instruction("add rsp, 16"); // release the conversion spill frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the missing-entry result pair + emitter.blank(); +} + /// Emits the Linux x86_64 stream runtime helper for getservbyport. fn emit_getservbyport_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); diff --git a/src/codegen_support/runtime/io/http_response.rs b/src/codegen_support/runtime/io/http_response.rs index f7dae85bbb..5eae71dc7f 100644 --- a/src/codegen_support/runtime/io/http_response.rs +++ b/src/codegen_support/runtime/io/http_response.rs @@ -8,7 +8,7 @@ //! - The EIR lowering of the `http_response_code` / `header` builtins calls these labels. //! //! Key details: -//! - In a `--web` build each routine forwards to a bridge setter via `bl_c` +//! - In a `--web` build each routine forwards to a native bridge setter //! (`elephc_web_set_status` / `elephc_web_header`); in a non-web build it is a //! no-op and never names the bridge symbols, so non-web binaries link without them. //! - `__rt_http_response_code`: status code in the first int arg register @@ -40,7 +40,7 @@ pub fn emit_http_response_code(emitter: &mut Emitter, web: bool) { emitter.instruction("str x30, [sp]"); // save the caller return address before the nested call if web { - emitter.bl_c("elephc_web_set_status"); // x0 = previous status; sets the status when the code is > 0 + emitter.emit_native_bridge_symbol_call("elephc_web_set_status", 1); // x0 = previous status; sets the status when the code is > 0 } else { emitter.instruction("mov x0, #0"); // non-web: no status machinery, return 0 } @@ -50,7 +50,7 @@ pub fn emit_http_response_code(emitter: &mut Emitter, web: bool) { emitter.instruction("ret"); // return to the caller (result in x0) } -/// Emits the x86_64 Linux variant of `__rt_http_response_code`. +/// Emits the x86_64 variant of `__rt_http_response_code`. fn emit_http_response_code_x86_64(emitter: &mut Emitter, web: bool) { emitter.blank(); emitter.comment("--- runtime: http_response_code ---"); @@ -60,7 +60,7 @@ fn emit_http_response_code_x86_64(emitter: &mut Emitter, web: bool) { emitter.instruction("mov rbp, rsp"); // establish a stable frame base if web { - emitter.bl_c("elephc_web_set_status"); // rax = previous status; sets the status when the code is > 0 + emitter.emit_native_bridge_symbol_call("elephc_web_set_status", 1); // rax = previous status; Windows remaps the SysV-staged status argument } else { emitter.instruction("xor eax, eax"); // non-web: no status machinery, return 0 } @@ -89,14 +89,14 @@ pub fn emit_header(emitter: &mut Emitter, web: bool) { if web { emitter.instruction("stp x29, x30, [sp, #-16]!"); // save frame/link registers (the forward call clobbers x30) emitter.instruction("mov x29, sp"); // establish a frame pointer for the call - emitter.bl_c("elephc_web_header"); // forward (x0=ptr, x1=len, x2=replace, x3=code) to the bridge + emitter.emit_native_bridge_symbol_call("elephc_web_header", 4); // forward (x0=ptr, x1=len, x2=replace, x3=code) to the bridge emitter.instruction("ldp x29, x30, [sp], #16"); // restore frame/link registers } emitter.instruction("ret"); // return to the caller (header() is void) } -/// Emits the x86_64 Linux variant of `__rt_header`. +/// Emits the x86_64 variant of `__rt_header`. fn emit_header_x86_64(emitter: &mut Emitter, web: bool) { emitter.blank(); emitter.comment("--- runtime: header ---"); @@ -105,9 +105,62 @@ fn emit_header_x86_64(emitter: &mut Emitter, web: bool) { if web { emitter.instruction("push rbp"); // preserve the caller frame pointer and align rsp for the call emitter.instruction("mov rbp, rsp"); // establish a stable frame base - emitter.bl_c("elephc_web_header"); // forward (rdi=ptr, rsi=len, rdx=replace, rcx=code) to the bridge + emitter.emit_native_bridge_symbol_call("elephc_web_header", 4); // forward SysV-staged args through the native C-ABI transition emitter.instruction("pop rbp"); // restore the caller frame pointer } emitter.instruction("ret"); // return to the caller (header() is void) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::codegen_support::platform::{Platform, Target}; + + /// Verifies Windows status forwarding remaps the SysV argument and brackets + /// the native call with mandatory MSx64 shadow space. + #[test] + fn windows_http_response_code_uses_native_bridge_transition() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_http_response_code(&mut emitter, true); + let asm = emitter.output(); + assert!(asm.contains("lea r11, [rip + elephc_web_set_status]")); + assert!(asm.contains("sub rsp, 32")); + assert!(asm.contains("mov rcx, rdi")); + assert!(asm.contains("call r11")); + assert!(asm.contains("add rsp, 32")); + assert!(!asm.contains("call elephc_web_set_status")); + } + + /// Verifies the four-argument header bridge transition preserves the fourth + /// SysV argument before lower-index MSx64 register moves clobber it. + #[test] + fn windows_header_uses_four_argument_native_bridge_transition() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_header(&mut emitter, true); + let asm = emitter.output(); + let save_fourth = asm.find("mov r9, rcx").expect("fourth argument remap"); + let replace_first = asm.find("mov rcx, rdi").expect("first argument remap"); + assert!(save_fourth < replace_first, "{asm}"); + assert!(asm.contains("sub rsp, 32")); + assert!(asm.contains("call r11")); + assert!(asm.contains("add rsp, 32")); + assert!(!asm.contains("call elephc_web_header")); + } + + /// Verifies Linux x86 keeps direct bridge calls and never adds Windows-only staging. + #[test] + fn linux_x86_web_calls_remain_direct() { + let mut status = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_http_response_code(&mut status, true); + let status_asm = status.output(); + assert!(status_asm.contains("call elephc_web_set_status")); + assert!(!status_asm.contains("sub rsp, 32")); + + let mut header = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_header(&mut header, true); + let header_asm = header.output(); + assert!(header_asm.contains("call elephc_web_header")); + assert!(!header_asm.contains("sub rsp, 32")); + } +} diff --git a/src/codegen_support/runtime/io/https.rs b/src/codegen_support/runtime/io/https.rs index 53eb0e6877..a406490615 100644 --- a/src/codegen_support/runtime/io/https.rs +++ b/src/codegen_support/runtime/io/https.rs @@ -12,7 +12,8 @@ //! `CRLFCRLF` header/body separator, and copies the body into an anonymous //! temp file whose descriptor is the readable stream. //! - The response is buffered in the 1 MiB `_https_resp_buf`; a larger response -//! is truncated. HTTP/1.0 + `Connection: close` keeps the body close-framed. +//! fails rather than being returned as a silently truncated stream. HTTP/1.0 + +//! `Connection: close` keeps the body close-framed. //! - All elephc-tls C entry points are invoked through the runtime function //! pointers (`_elephc_tls_*_fn`) so the shared runtime carries no direct //! elephc-tls symbol reference — only programs that actually open https URLs @@ -37,145 +38,130 @@ pub fn emit_https(emitter: &mut Emitter) { emitter.comment("--- runtime: https_open ---"); emitter.label_global("__rt_https_open"); - // Frame (80 bytes): [0]=tls handle [8]=req ptr [16]=req len - // [24]=response len [32]=body start [40]=temp fd. - emitter.instruction("sub sp, sp, #80"); // allocate the helper 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 + // Frame (192 bytes): response bookkeeping at 0..40, the stable 88-byte + // ElephcTlsClientOptions at 48..135, bool scratch at 136, saved connection + // inputs at 144..160, I/O progress/failure state at 168, and the saved + // frame pair at 176..184. + emitter.instruction("sub sp, sp, #192"); // allocate response, TLS options, and connection spill storage + emitter.instruction("stp x29, x30, [sp, #176]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #176"); // establish the helper frame pointer emitter.instruction("str x3, [sp, #8]"); // save the HTTP request pointer emitter.instruction("str x4, [sp, #16]"); // save the HTTP request length + emitter.instruction("str x0, [sp, #144]"); // save the connection host pointer + emitter.instruction("str x1, [sp, #152]"); // save the connection host length + emitter.instruction("str x2, [sp, #160]"); // save the connection port - // -- elephc_tls_connect(host, host_len, port, cafile_ptr, cafile_len) -- - // Pick the connect variant from the ssl context: ssl.cafile (custom CA - // bundle) wins, else ssl.verify_peer = "0" (insecure), else the secure - // default. The call always passes the cafile args; the secure/insecure - // variants ignore them, so one call site serves all three. - emitter.instruction("stp x0, x1, [sp, #-16]!"); // save host_ptr/host_len across the context lookups - emitter.instruction("str x2, [sp, #-16]!"); // save port (16 bytes for stack alignment) - emitter.instruction("sub sp, sp, #16"); // string-lookup out slots: [sp+0]=ptr, [sp+8]=len - // ssl.cafile lookup — a custom CA bundle path - emitter.instruction("str xzr, [sp, #0]"); // out ptr default - emitter.instruction("str xzr, [sp, #8]"); // out len default - abi::emit_symbol_address(emitter, "x0", "_ssl_key_str"); - emitter.instruction("mov x1, #3"); // strlen("ssl") - abi::emit_symbol_address(emitter, "x2", "_ssl_cafile_key_str"); - emitter.instruction("mov x3, #6"); // strlen("cafile") - emitter.instruction("mov x4, sp"); // out_ptr_addr - emitter.instruction("add x5, sp, #8"); // out_len_addr - emitter.instruction("bl __rt_get_string_context_option"); // x0 = 1 on hit - emitter.instruction("cbz x0, __rt_https_open_no_cafile_aarch64"); // no cafile → verify_peer dispatch - emitter.instruction("ldr x3, [sp, #0]"); // cafile path ptr → connect arg 3 - emitter.instruction("ldr x4, [sp, #8]"); // cafile path len → connect arg 4 - abi::emit_symbol_address(emitter, "x9", "_elephc_tls_connect_cafile_fn"); - emitter.instruction("ldr x9, [x9]"); // cafile connect variant address - emitter.instruction("b __rt_https_open_have_fn_aarch64"); // continue at target label - emitter.label("__rt_https_open_no_cafile_aarch64"); - // ssl.capath lookup — a directory of CA certificates (checked after cafile) - emitter.instruction("str xzr, [sp, #0]"); // reset out ptr - emitter.instruction("str xzr, [sp, #8]"); // reset out len - abi::emit_symbol_address(emitter, "x0", "_ssl_key_str"); - emitter.instruction("mov x1, #3"); // strlen("ssl") - abi::emit_symbol_address(emitter, "x2", "_ssl_capath_key_str"); - emitter.instruction("mov x3, #6"); // strlen("capath") - emitter.instruction("mov x4, sp"); // out_ptr_addr - emitter.instruction("add x5, sp, #8"); // out_len_addr - emitter.instruction("bl __rt_get_string_context_option"); // x0 = 1 on hit - emitter.instruction("cbz x0, __rt_https_open_no_capath_aarch64"); // no capath → verify_peer dispatch - emitter.instruction("ldr x3, [sp, #0]"); // capath dir ptr → connect arg 3 - emitter.instruction("ldr x4, [sp, #8]"); // capath dir len → connect arg 4 - abi::emit_symbol_address(emitter, "x9", "_elephc_tls_connect_capath_fn"); - emitter.instruction("ldr x9, [x9]"); // capath connect variant address - emitter.instruction("b __rt_https_open_have_fn_aarch64"); // continue at target label - emitter.label("__rt_https_open_no_capath_aarch64"); - // ssl.verify_peer = "0" / ssl.allow_self_signed / ssl.verify_peer_name = "0" - // → insecure variant (encrypted, peer identity relaxed); else secure default - emitter.instruction("str xzr, [sp, #0]"); // reset out ptr - emitter.instruction("str xzr, [sp, #8]"); // reset out len + // -- initialize ElephcTlsClientOptions v1 (88 bytes at sp + 48) -- + for offset in (48..=128).step_by(8) { + emitter.instruction(&format!("str xzr, [sp, #{offset}]")); // zero one TLS options word + } + emitter.instruction("mov w9, #1"); // select ElephcTlsClientOptions ABI v1 + emitter.instruction("str w9, [sp, #48]"); // store options.abi_version at byte offset 0 + emitter.instruction("mov w9, #3"); // default to verify_peer plus verify_peer_name + emitter.instruction("str w9, [sp, #52]"); // store options.policy at byte offset 4 + + // -- collect all combinable string options directly into the ABI struct -- + for (key, key_len, ptr_offset, len_offset) in [ + ("_ssl_peer_name_key_str", 9, 56, 64), + ("_ssl_cafile_key_str", 6, 72, 80), + ("_ssl_capath_key_str", 6, 88, 96), + ("_ssl_local_cert_key_str", 10, 104, 112), + ("_ssl_local_pk_key_str", 8, 120, 128), + ] { + abi::emit_symbol_address(emitter, "x0", "_ssl_key_str"); + emitter.instruction("mov x1, #3"); // pass strlen("ssl") + abi::emit_symbol_address(emitter, "x2", key); + emitter.instruction(&format!("mov x3, #{key_len}")); // pass the TLS option-key length + emitter.instruction(&format!("add x4, sp, #{ptr_offset}")); // pass the destination pointer-field address + emitter.instruction(&format!("add x5, sp, #{len_offset}")); // pass the destination length-field address + emitter.instruction("bl __rt_get_string_context_option"); // populate this optional string field when present + } + + // -- resolve verify_peer with PHP scalar truthiness (default true) -- + emitter.instruction("mov x9, #1"); // default verify_peer to true for TLS clients + emitter.instruction("str x9, [sp, #136]"); // seed the scalar lookup output abi::emit_symbol_address(emitter, "x0", "_ssl_key_str"); - emitter.instruction("mov x1, #3"); // strlen("ssl") + emitter.instruction("mov x1, #3"); // pass strlen("ssl") abi::emit_symbol_address(emitter, "x2", "_ssl_verify_peer_key_str"); - emitter.instruction("mov x3, #11"); // strlen("verify_peer") - emitter.instruction("mov x4, sp"); // out_ptr_addr - emitter.instruction("add x5, sp, #8"); // out_len_addr - emitter.instruction("bl __rt_get_string_context_option"); // x0 = 1 on hit - emitter.instruction("cbz x0, __rt_https_open_self_signed_aarch64"); // verify_peer miss → check allow_self_signed - emitter.instruction("ldr x9, [sp, #0]"); // verify_peer string ptr - emitter.instruction("ldrb w10, [x9]"); // first byte - emitter.instruction("cmp w10, #48"); // '0'? - emitter.instruction("b.eq __rt_https_open_insecure_aarch64"); // "0" → relaxed (insecure) variant - // ssl.allow_self_signed present (any value) → relaxed peer verification - emitter.label("__rt_https_open_self_signed_aarch64"); - emitter.instruction("str xzr, [sp, #0]"); // reset out ptr - emitter.instruction("str xzr, [sp, #8]"); // reset out len - abi::emit_symbol_address(emitter, "x0", "_ssl_key_str"); - emitter.instruction("mov x1, #3"); // strlen("ssl") - abi::emit_symbol_address(emitter, "x2", "_ssl_allow_self_signed_key_str"); - emitter.instruction("mov x3, #17"); // strlen("allow_self_signed") - emitter.instruction("mov x4, sp"); // out_ptr_addr - emitter.instruction("add x5, sp, #8"); // out_len_addr - emitter.instruction("bl __rt_get_string_context_option"); // x0 = 1 on hit - emitter.instruction("cbnz x0, __rt_https_open_insecure_aarch64"); // present → relaxed (insecure) variant - // ssl.verify_peer_name = "0" → relaxed peer verification - emitter.instruction("str xzr, [sp, #0]"); // reset out ptr - emitter.instruction("str xzr, [sp, #8]"); // reset out len + emitter.instruction("mov x3, #11"); // pass strlen("verify_peer") + emitter.instruction("add x4, sp, #136"); // pass the verify_peer output address + emitter.instruction("bl __rt_get_bool_context_option"); // accept bool/int/string PHP truthiness + emitter.instruction("ldr w9, [sp, #136]"); // load the normalized verify_peer value + emitter.instruction("and w9, w9, #1"); // constrain the policy contribution to one bit + emitter.instruction("ldr w10, [sp, #52]"); // load the current TLS policy bitmask + emitter.instruction("bic w10, w10, #1"); // clear the previous verify_peer bit + emitter.instruction("orr w10, w10, w9"); // merge the resolved verify_peer bit + emitter.instruction("str w10, [sp, #52]"); // persist the updated TLS policy + + // -- resolve verify_peer_name independently (default true) -- + emitter.instruction("mov x9, #1"); // default verify_peer_name to true for TLS clients + emitter.instruction("str x9, [sp, #136]"); // seed the scalar lookup output abi::emit_symbol_address(emitter, "x0", "_ssl_key_str"); - emitter.instruction("mov x1, #3"); // strlen("ssl") + emitter.instruction("mov x1, #3"); // pass strlen("ssl") abi::emit_symbol_address(emitter, "x2", "_ssl_verify_peer_name_key_str"); - emitter.instruction("mov x3, #16"); // strlen("verify_peer_name") - emitter.instruction("mov x4, sp"); // out_ptr_addr - emitter.instruction("add x5, sp, #8"); // out_len_addr - emitter.instruction("bl __rt_get_string_context_option"); // x0 = 1 on hit - emitter.instruction("cbz x0, __rt_https_open_peer_name_aarch64"); // miss → check peer_name - emitter.instruction("ldr x9, [sp, #0]"); // verify_peer_name string ptr - emitter.instruction("ldrb w10, [x9]"); // first byte - emitter.instruction("cmp w10, #48"); // '0'? - emitter.instruction("b.eq __rt_https_open_insecure_aarch64"); // "0" → relaxed (insecure) variant - // ssl.peer_name set → verify the certificate for that name (secure) - emitter.label("__rt_https_open_peer_name_aarch64"); - emitter.instruction("str xzr, [sp, #0]"); // reset out ptr - emitter.instruction("str xzr, [sp, #8]"); // reset out len + emitter.instruction("mov x3, #16"); // pass strlen("verify_peer_name") + emitter.instruction("add x4, sp, #136"); // pass the verify_peer_name output address + emitter.instruction("bl __rt_get_bool_context_option"); // accept bool/int/string PHP truthiness + emitter.instruction("ldr w9, [sp, #136]"); // load the normalized verify_peer_name value + emitter.instruction("and w9, w9, #1"); // constrain the policy contribution to one bit + emitter.instruction("lsl w9, w9, #1"); // move the value into policy bit one + emitter.instruction("ldr w10, [sp, #52]"); // load the current TLS policy bitmask + emitter.instruction("bic w10, w10, #2"); // clear the previous verify_peer_name bit + emitter.instruction("orr w10, w10, w9"); // merge the resolved verify_peer_name bit + emitter.instruction("str w10, [sp, #52]"); // persist the updated TLS policy + + // -- resolve allow_self_signed independently (default false) -- + emitter.instruction("str xzr, [sp, #136]"); // default allow_self_signed to false abi::emit_symbol_address(emitter, "x0", "_ssl_key_str"); - emitter.instruction("mov x1, #3"); // strlen("ssl") - abi::emit_symbol_address(emitter, "x2", "_ssl_peer_name_key_str"); - emitter.instruction("mov x3, #9"); // strlen("peer_name") - emitter.instruction("mov x4, sp"); // out_ptr_addr - emitter.instruction("add x5, sp, #8"); // out_len_addr - emitter.instruction("bl __rt_get_string_context_option"); // x0 = 1 on hit - emitter.instruction("cbz x0, __rt_https_open_secure_aarch64"); // no peer_name → secure default - emitter.instruction("ldr x3, [sp, #0]"); // peer_name ptr → connect arg 3 - emitter.instruction("ldr x4, [sp, #8]"); // peer_name len → connect arg 4 - abi::emit_symbol_address(emitter, "x9", "_elephc_tls_connect_peer_name_fn"); - emitter.instruction("ldr x9, [x9]"); // peer_name connect variant address - emitter.instruction("b __rt_https_open_have_fn_aarch64"); // continue at target label - emitter.label("__rt_https_open_insecure_aarch64"); - abi::emit_symbol_address(emitter, "x9", "_elephc_tls_connect_insecure_fn"); - emitter.instruction("ldr x9, [x9]"); // insecure variant address - emitter.instruction("mov x3, #0"); // no cafile/capath/peer_name path - emitter.instruction("mov x4, #0"); // no cafile/capath/peer_name path - emitter.instruction("b __rt_https_open_have_fn_aarch64"); // continue at target label - emitter.label("__rt_https_open_secure_aarch64"); - abi::emit_symbol_address(emitter, "x9", "_elephc_tls_connect_fn"); - emitter.instruction("ldr x9, [x9]"); // secure variant address - emitter.instruction("mov x3, #0"); // no cafile/capath/peer_name path - emitter.instruction("mov x4, #0"); // no cafile/capath/peer_name path - emitter.label("__rt_https_open_have_fn_aarch64"); - emitter.instruction("add sp, sp, #16"); // discard the string-lookup out slots - emitter.instruction("ldr x2, [sp], #16"); // restore port - emitter.instruction("ldp x0, x1, [sp], #16"); // restore host_ptr/host_len + emitter.instruction("mov x1, #3"); // pass strlen("ssl") + abi::emit_symbol_address(emitter, "x2", "_ssl_allow_self_signed_key_str"); + emitter.instruction("mov x3, #17"); // pass strlen("allow_self_signed") + emitter.instruction("add x4, sp, #136"); // pass the allow_self_signed output address + emitter.instruction("bl __rt_get_bool_context_option"); // accept bool/int/string PHP truthiness + emitter.instruction("ldr w9, [sp, #136]"); // load the normalized allow_self_signed value + emitter.instruction("and w9, w9, #1"); // constrain the policy contribution to one bit + emitter.instruction("lsl w9, w9, #2"); // move the value into policy bit two + emitter.instruction("ldr w10, [sp, #52]"); // load the current TLS policy bitmask + emitter.instruction("bic w10, w10, #4"); // clear the previous allow_self_signed bit + emitter.instruction("orr w10, w10, w9"); // merge the resolved allow_self_signed bit + emitter.instruction("str w10, [sp, #52]"); // persist the updated TLS policy + + // -- connect_with_options(host, host_len, port, &options) -- + abi::emit_symbol_address(emitter, "x9", "_elephc_tls_connect_with_options_fn"); + emitter.instruction("ldr x9, [x9]"); // load the policy-aware TLS connect entry + emitter.instruction("ldr x0, [sp, #144]"); // restore the connection host pointer + emitter.instruction("ldr x1, [sp, #152]"); // restore the connection host length + emitter.instruction("ldr x2, [sp, #160]"); // restore the connection port + emitter.instruction("add x3, sp, #48"); // pass the stable TLS options struct address emitter.instruction("cbz x9, __rt_https_open_fail"); // missing elephc-tls runtime means HTTPS open fails closed - emitter.instruction("blr x9"); // open the TLS session, x0 = handle + emitter.emit_published_bridge_call("x9"); // open the TLS session through the published ABI entry, x0 = handle emitter.instruction("cmp x0, #0"); // did the TLS handshake fail? emitter.instruction("b.lt __rt_https_open_fail"); // negative handle means TLS connect failed emitter.instruction("str x0, [sp, #0]"); // save the TLS session handle - // -- elephc_tls_write(handle, req_ptr, req_len) -- - emitter.instruction("ldr x1, [sp, #8]"); // request pointer for the TLS write - emitter.instruction("ldr x2, [sp, #16]"); // request length for the TLS write + // -- write the complete HTTP request, preserving partial TLS writes -- + emitter.instruction("str xzr, [sp, #168]"); // request write offset starts at zero + emitter.label("__rt_https_open_write"); + emitter.instruction("ldr x9, [sp, #168]"); // reload bytes already sent through TLS + emitter.instruction("ldr x10, [sp, #16]"); // reload the full request length + emitter.instruction("cmp x9, x10"); // has every request byte been written? + emitter.instruction("b.ge __rt_https_open_read_start"); // begin reading only after a complete request write + emitter.instruction("ldr x0, [sp, #0]"); // pass the TLS session handle + emitter.instruction("ldr x1, [sp, #8]"); // reload the HTTP request pointer + emitter.instruction("add x1, x1, x9"); // advance past request bytes already sent + emitter.instruction("sub x2, x10, x9"); // pass only the unwritten request suffix abi::emit_symbol_address(emitter, "x9", "_elephc_tls_write_fn"); emitter.instruction("ldr x9, [x9]"); // load the elephc_tls_write entry pointer - emitter.instruction("blr x9"); // send the HTTP request through TLS + emitter.emit_published_bridge_call("x9"); // send the HTTP request through the published TLS entry + emitter.instruction("cmp x0, #0"); // TLS write must make forward progress + emitter.instruction("b.le __rt_https_open_tls_failed"); // errors, WouldBlock, TimedOut, and zero all fail this blocking wrapper + emitter.instruction("ldr x9, [sp, #168]"); // reload the completed write offset + emitter.instruction("add x9, x9, x0"); // account for this partial TLS write + emitter.instruction("str x9, [sp, #168]"); // retain progress for the next write iteration + emitter.instruction("b __rt_https_open_write"); // continue until the complete request is sent // -- read the whole TLS-decrypted response into _https_resp_buf -- + emitter.label("__rt_https_open_read_start"); emitter.instruction("str xzr, [sp, #24]"); // accumulated response length = 0 emitter.label("__rt_https_open_read"); emitter.instruction("ldr x0, [sp, #0]"); // TLS handle for the read @@ -184,23 +170,32 @@ pub fn emit_https(emitter: &mut Emitter) { emitter.instruction("add x1, x1, x9"); // read into the buffer past the buffered bytes emitter.instruction(&format!("mov x2, #{}", HTTPS_RESP_BUF_SIZE)); // response buffer capacity emitter.instruction("subs x2, x2, x9"); // remaining buffer capacity - emitter.instruction("b.le __rt_https_open_read_done"); // stop when the response buffer is full + emitter.instruction("b.le __rt_https_open_tls_failed"); // fail instead of returning a truncated response body abi::emit_symbol_address(emitter, "x9", "_elephc_tls_read_fn"); emitter.instruction("ldr x9, [x9]"); // load the elephc_tls_read entry pointer - emitter.instruction("blr x9"); // read more decrypted bytes from the TLS session - emitter.instruction("cmp x0, #0"); // did the read hit EOF or fail? - emitter.instruction("b.le __rt_https_open_read_done"); // the server closed the TLS session + emitter.emit_published_bridge_call("x9"); // read decrypted bytes through the published TLS entry + emitter.instruction("cmp x0, #0"); // distinguish a clean EOF from every TLS error sentinel + emitter.instruction("b.lt __rt_https_open_tls_failed"); // terminal, WouldBlock, and TimedOut reads must not become EOF + emitter.instruction("b.eq __rt_https_open_read_done"); // a zero-byte TLS read is the sole successful EOF emitter.instruction("ldr x9, [sp, #24]"); // reload the accumulated response length emitter.instruction("add x9, x9, x0"); // advance by the bytes just read emitter.instruction("str x9, [sp, #24]"); // store the updated response length emitter.instruction("b __rt_https_open_read"); // continue reading the TLS response emitter.label("__rt_https_open_read_done"); + emitter.instruction("str xzr, [sp, #168]"); // mark clean TLS completion before closing the session + emitter.instruction("b __rt_https_open_tls_close"); // release the TLS handle before materializing the stream + emitter.label("__rt_https_open_tls_failed"); + emitter.instruction("mov x9, #1"); // remember that I/O failed while the handle still needs cleanup + emitter.instruction("str x9, [sp, #168]"); // preserve failure state across the TLS close call // -- elephc_tls_close(handle): send close_notify and drop the session -- + emitter.label("__rt_https_open_tls_close"); emitter.instruction("ldr x0, [sp, #0]"); // TLS handle for the close abi::emit_symbol_address(emitter, "x9", "_elephc_tls_close_fn"); emitter.instruction("ldr x9, [x9]"); // load the elephc_tls_close entry pointer - emitter.instruction("blr x9"); // shut the TLS session down cleanly + emitter.emit_published_bridge_call("x9"); // shut the TLS session down through the published TLS entry + emitter.instruction("ldr x9, [sp, #168]"); // recover the pre-close I/O status + emitter.instruction("cbnz x9, __rt_https_open_fail"); // never materialize a partial response after TLS I/O failed // -- scan for the CRLFCRLF that separates headers from the body -- abi::emit_symbol_address(emitter, "x4", "_https_resp_buf"); @@ -240,29 +235,60 @@ pub fn emit_https(emitter: &mut Emitter) { emitter.instruction("b.lt __rt_https_open_fail"); // propagate the failure emitter.instruction("str x0, [sp, #40]"); // save the temp-file descriptor - // -- write(temp, body, body length) -- + // -- write the complete body into the temporary stream -- + emitter.instruction("str xzr, [sp, #168]"); // temporary-file write offset starts at zero + emitter.label("__rt_https_open_body_write"); + emitter.instruction("ldr x9, [sp, #168]"); // reload bytes already copied to the temporary file + emitter.instruction("ldr x10, [sp, #24]"); // reload the full response length + emitter.instruction("ldr x11, [sp, #32]"); // reload the response-body start offset + emitter.instruction("sub x10, x10, x11"); // derive the complete body length + emitter.instruction("cmp x9, x10"); // has the full response body reached the tempfile? + emitter.instruction("b.ge __rt_https_open_body_written"); // continue only after every body byte was persisted + emitter.instruction("ldr x0, [sp, #40]"); // pass the anonymous temporary-file descriptor abi::emit_symbol_address(emitter, "x1", "_https_resp_buf"); - emitter.instruction("ldr x9, [sp, #32]"); // body start offset - emitter.instruction("add x1, x1, x9"); // body pointer = buffer + body start - emitter.instruction("ldr x10, [sp, #24]"); // response length - emitter.instruction("sub x2, x10, x9"); // body length = response length - body start + emitter.instruction("add x1, x1, x11"); // point at the beginning of the response body + emitter.instruction("add x1, x1, x9"); // advance past body bytes already copied + emitter.instruction("sub x2, x10, x9"); // pass the remaining body suffix emitter.syscall(4); + if emitter.platform.needs_cmp_before_error_branch() { + emitter.instruction("cmp x0, #0"); // Linux: a negative write result means tempfile output failed + } + emitter.instruction(&emitter.platform.branch_on_syscall_success("__rt_https_open_body_write_result")); // continue only after a successful tempfile write + emitter.instruction("b __rt_https_open_temp_fail"); // discard the tempfile after a write failure + emitter.label("__rt_https_open_body_write_result"); + emitter.instruction("cbz x0, __rt_https_open_temp_fail"); // a zero-byte write cannot complete the remaining body + emitter.instruction("ldr x9, [sp, #168]"); // reload the copied-byte count + emitter.instruction("add x9, x9, x0"); // account for this partial tempfile write + emitter.instruction("str x9, [sp, #168]"); // retain progress for the next copy iteration + emitter.instruction("b __rt_https_open_body_write"); // continue until the entire body is copied + emitter.label("__rt_https_open_body_written"); // -- lseek(temp, 0, SEEK_SET): rewind so the stream reads from the start -- emitter.instruction("ldr x0, [sp, #40]"); // reload the temp-file descriptor emitter.instruction("mov x1, #0"); // offset = 0 emitter.instruction("mov x2, #0"); // whence = SEEK_SET emitter.syscall(199); + if emitter.platform.needs_cmp_before_error_branch() { + emitter.instruction("cmp x0, #0"); // Linux: a negative seek result means the tempfile is unusable + } + emitter.instruction(&emitter.platform.branch_on_syscall_success("__rt_https_open_rewound")); // return the tempfile only after a successful rewind + emitter.instruction("b __rt_https_open_temp_fail"); // close an unreadable temporary stream before failing + emitter.label("__rt_https_open_rewound"); emitter.instruction("ldr x0, [sp, #40]"); // return the rewound body descriptor - emitter.instruction("ldp x29, x30, [sp, #64]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #80"); // release the helper frame + emitter.instruction("ldp x29, x30, [sp, #176]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #192"); // release the helper frame emitter.instruction("ret"); // return the https:// stream descriptor + emitter.label("__rt_https_open_temp_fail"); + emitter.instruction("ldr x0, [sp, #40]"); // close the temporary descriptor before reporting failure + emitter.syscall(6); + emitter.instruction("b __rt_https_open_fail"); // reuse the common failure epilogue after cleanup + emitter.label("__rt_https_open_fail"); emitter.instruction("mov x0, #-1"); // -1 signals a failed https:// open - emitter.instruction("ldp x29, x30, [sp, #64]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #80"); // release the helper frame + emitter.instruction("ldp x29, x30, [sp, #176]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #192"); // release the helper frame emitter.instruction("ret"); // return the failure result } @@ -272,145 +298,121 @@ fn emit_https_linux_x86_64(emitter: &mut Emitter) { emitter.comment("--- runtime: https_open ---"); emitter.label_global("__rt_https_open"); - // Frame (rbp-relative): [-8]=tls handle [-16]=host ptr [-24]=host len - // [-32]=port [-40]=req ptr [-48]=req len - // [-56]=response len [-64]=body start [-72]=temp fd. + // Frame (rbp-relative): response bookkeeping at -8..-80, the stable + // 88-byte ElephcTlsClientOptions at -176..-89, and bool scratch at -184. emitter.instruction("push rbp"); // preserve the caller frame pointer emitter.instruction("mov rbp, rsp"); // establish the helper frame pointer - emitter.instruction("sub rsp, 96"); // reserve the helper spill slots (96 = +16 for verify_peer) + emitter.instruction("sub rsp, 192"); // reserve aligned response, TLS options, and scalar scratch storage emitter.instruction("mov QWORD PTR [rbp - 16], rdi"); // save the host pointer emitter.instruction("mov QWORD PTR [rbp - 24], rsi"); // save the host length emitter.instruction("mov QWORD PTR [rbp - 32], rdx"); // save the port number emitter.instruction("mov QWORD PTR [rbp - 40], rcx"); // save the request pointer emitter.instruction("mov QWORD PTR [rbp - 48], r8"); // save the request length - // -- pick the connect fn from the ssl context: cafile, else verify_peer -- - // The call always passes cafile args (rcx/r8); the secure/insecure - // variants ignore them, so one call site serves all three. - emitter.instruction("mov QWORD PTR [rbp - 80], 0"); // string-lookup out ptr default - emitter.instruction("mov QWORD PTR [rbp - 88], 0"); // string-lookup out len default - abi::emit_symbol_address(emitter, "rdi", "_ssl_key_str"); // load runtime data address - emitter.instruction("mov rsi, 3"); // strlen("ssl") - abi::emit_symbol_address(emitter, "rdx", "_ssl_cafile_key_str"); // load runtime data address - emitter.instruction("mov rcx, 6"); // strlen("cafile") - emitter.instruction("lea r8, [rbp - 80]"); // out_ptr_addr - emitter.instruction("lea r9, [rbp - 88]"); // out_len_addr - emitter.instruction("call __rt_get_string_context_option"); // rax = 1 on hit - emitter.instruction("test rax, rax"); // check whether the runtime value is zero - emitter.instruction("jz __rt_https_open_no_cafile_x"); // no cafile → verify_peer dispatch - abi::emit_load_symbol_to_reg(emitter, "r9", "_elephc_tls_connect_cafile_fn", 0); // cafile connect variant - emitter.instruction("mov rcx, QWORD PTR [rbp - 80]"); // cafile path ptr → connect arg rcx - emitter.instruction("mov r8, QWORD PTR [rbp - 88]"); // cafile path len → connect arg r8 - emitter.instruction("jmp __rt_https_open_have_fn_x"); // continue at target label - emitter.label("__rt_https_open_no_cafile_x"); - // ssl.capath lookup — a directory of CA certificates (checked after cafile) - emitter.instruction("mov QWORD PTR [rbp - 80], 0"); // reset out ptr - emitter.instruction("mov QWORD PTR [rbp - 88], 0"); // reset out len - abi::emit_symbol_address(emitter, "rdi", "_ssl_key_str"); // load runtime data address - emitter.instruction("mov rsi, 3"); // strlen("ssl") - abi::emit_symbol_address(emitter, "rdx", "_ssl_capath_key_str"); // load runtime data address - emitter.instruction("mov rcx, 6"); // strlen("capath") - emitter.instruction("lea r8, [rbp - 80]"); // out_ptr_addr - emitter.instruction("lea r9, [rbp - 88]"); // out_len_addr - emitter.instruction("call __rt_get_string_context_option"); // rax = 1 on hit - emitter.instruction("test rax, rax"); // check whether the runtime value is zero - emitter.instruction("jz __rt_https_open_no_capath_x"); // no capath → verify_peer dispatch - abi::emit_load_symbol_to_reg(emitter, "r9", "_elephc_tls_connect_capath_fn", 0); // capath connect variant - emitter.instruction("mov rcx, QWORD PTR [rbp - 80]"); // capath dir ptr → connect arg rcx - emitter.instruction("mov r8, QWORD PTR [rbp - 88]"); // capath dir len → connect arg r8 - emitter.instruction("jmp __rt_https_open_have_fn_x"); // continue at target label - emitter.label("__rt_https_open_no_capath_x"); - emitter.instruction("mov QWORD PTR [rbp - 80], 0"); // reset out ptr - emitter.instruction("mov QWORD PTR [rbp - 88], 0"); // reset out len - abi::emit_symbol_address(emitter, "rdi", "_ssl_key_str"); // load runtime data address - emitter.instruction("mov rsi, 3"); // strlen("ssl") - abi::emit_symbol_address(emitter, "rdx", "_ssl_verify_peer_key_str"); // load runtime data address - emitter.instruction("mov rcx, 11"); // strlen("verify_peer") - emitter.instruction("lea r8, [rbp - 80]"); // out_ptr_addr - emitter.instruction("lea r9, [rbp - 88]"); // out_len_addr - emitter.instruction("call __rt_get_string_context_option"); // rax = 1 on hit - emitter.instruction("test rax, rax"); // check whether the runtime value is zero - emitter.instruction("jz __rt_https_open_self_signed_x"); // verify_peer miss → check allow_self_signed - emitter.instruction("mov rcx, QWORD PTR [rbp - 80]"); // verify_peer ptr - emitter.instruction("movzx eax, BYTE PTR [rcx]"); // first byte of value - emitter.instruction("cmp al, 48"); // '0'? - emitter.instruction("je __rt_https_open_insecure_x"); // "0" → relaxed (insecure) variant - // ssl.allow_self_signed present (any value) → relaxed peer verification - emitter.label("__rt_https_open_self_signed_x"); - emitter.instruction("mov QWORD PTR [rbp - 80], 0"); // reset out ptr - emitter.instruction("mov QWORD PTR [rbp - 88], 0"); // reset out len - abi::emit_symbol_address(emitter, "rdi", "_ssl_key_str"); // load runtime data address - emitter.instruction("mov rsi, 3"); // strlen("ssl") - abi::emit_symbol_address(emitter, "rdx", "_ssl_allow_self_signed_key_str"); // load runtime data address - emitter.instruction("mov rcx, 17"); // strlen("allow_self_signed") - emitter.instruction("lea r8, [rbp - 80]"); // out_ptr_addr - emitter.instruction("lea r9, [rbp - 88]"); // out_len_addr - emitter.instruction("call __rt_get_string_context_option"); // rax = 1 on hit - emitter.instruction("test rax, rax"); // check whether the runtime value is zero - emitter.instruction("jnz __rt_https_open_insecure_x"); // present → relaxed (insecure) variant - // ssl.verify_peer_name = "0" → relaxed peer verification - emitter.instruction("mov QWORD PTR [rbp - 80], 0"); // reset out ptr - emitter.instruction("mov QWORD PTR [rbp - 88], 0"); // reset out len - abi::emit_symbol_address(emitter, "rdi", "_ssl_key_str"); // load runtime data address - emitter.instruction("mov rsi, 3"); // strlen("ssl") - abi::emit_symbol_address(emitter, "rdx", "_ssl_verify_peer_name_key_str"); // load runtime data address - emitter.instruction("mov rcx, 16"); // strlen("verify_peer_name") - emitter.instruction("lea r8, [rbp - 80]"); // out_ptr_addr - emitter.instruction("lea r9, [rbp - 88]"); // out_len_addr - emitter.instruction("call __rt_get_string_context_option"); // rax = 1 on hit - emitter.instruction("test rax, rax"); // check whether the runtime value is zero - emitter.instruction("jz __rt_https_open_peer_name_x"); // miss → check peer_name - emitter.instruction("mov rcx, QWORD PTR [rbp - 80]"); // verify_peer_name ptr - emitter.instruction("movzx eax, BYTE PTR [rcx]"); // first byte of value - emitter.instruction("cmp al, 48"); // '0'? - emitter.instruction("je __rt_https_open_insecure_x"); // "0" → relaxed (insecure) variant - // ssl.peer_name set → verify the certificate for that name (secure) - emitter.label("__rt_https_open_peer_name_x"); - emitter.instruction("mov QWORD PTR [rbp - 80], 0"); // reset out ptr - emitter.instruction("mov QWORD PTR [rbp - 88], 0"); // reset out len - abi::emit_symbol_address(emitter, "rdi", "_ssl_key_str"); // load runtime data address - emitter.instruction("mov rsi, 3"); // strlen("ssl") - abi::emit_symbol_address(emitter, "rdx", "_ssl_peer_name_key_str"); // load runtime data address - emitter.instruction("mov rcx, 9"); // strlen("peer_name") - emitter.instruction("lea r8, [rbp - 80]"); // out_ptr_addr - emitter.instruction("lea r9, [rbp - 88]"); // out_len_addr - emitter.instruction("call __rt_get_string_context_option"); // rax = 1 on hit - emitter.instruction("test rax, rax"); // check whether the runtime value is zero - emitter.instruction("jz __rt_https_open_secure_x"); // no peer_name → secure default - abi::emit_load_symbol_to_reg(emitter, "r9", "_elephc_tls_connect_peer_name_fn", 0); // peer_name connect variant - emitter.instruction("mov rcx, QWORD PTR [rbp - 80]"); // peer_name ptr → connect arg rcx - emitter.instruction("mov r8, QWORD PTR [rbp - 88]"); // peer_name len → connect arg r8 - emitter.instruction("jmp __rt_https_open_have_fn_x"); // rcx/r8 already hold the peer_name path - emitter.label("__rt_https_open_insecure_x"); - abi::emit_load_symbol_to_reg(emitter, "r9", "_elephc_tls_connect_insecure_fn", 0); // relaxed (insecure) variant - emitter.instruction("xor ecx, ecx"); // no cafile/capath/peer_name path - emitter.instruction("xor r8d, r8d"); // no cafile/capath/peer_name path - emitter.instruction("jmp __rt_https_open_have_fn_x"); // continue at target label - emitter.label("__rt_https_open_secure_x"); - abi::emit_load_symbol_to_reg(emitter, "r9", "_elephc_tls_connect_fn", 0); // secure default variant - emitter.instruction("xor ecx, ecx"); // no cafile/capath/peer_name path - emitter.instruction("xor r8d, r8d"); // no cafile/capath/peer_name path - emitter.label("__rt_https_open_have_fn_x"); - - // -- connect(host, host_len, port, cafile_ptr, cafile_len) — through r9 -- + // -- initialize ElephcTlsClientOptions v1 (88 bytes at rbp - 176) -- + for offset in (96..=176).step_by(8) { + emitter.instruction(&format!("mov QWORD PTR [rbp - {offset}], 0")); // zero one TLS options word + } + emitter.instruction("mov DWORD PTR [rbp - 176], 1"); // store options.abi_version at byte offset 0 + emitter.instruction("mov DWORD PTR [rbp - 172], 3"); // default policy verifies the peer and its name + + // -- collect all combinable string options directly into the ABI struct -- + for (key, key_len, ptr_offset, len_offset) in [ + ("_ssl_peer_name_key_str", 9, 168, 160), + ("_ssl_cafile_key_str", 6, 152, 144), + ("_ssl_capath_key_str", 6, 136, 128), + ("_ssl_local_cert_key_str", 10, 120, 112), + ("_ssl_local_pk_key_str", 8, 104, 96), + ] { + abi::emit_symbol_address(emitter, "rdi", "_ssl_key_str"); // pass the TLS context wrapper name + emitter.instruction("mov rsi, 3"); // pass strlen("ssl") + abi::emit_symbol_address(emitter, "rdx", key); + emitter.instruction(&format!("mov rcx, {key_len}")); // pass the TLS option-key length + emitter.instruction(&format!("lea r8, [rbp - {ptr_offset}]")); // pass the destination pointer-field address + emitter.instruction(&format!("lea r9, [rbp - {len_offset}]")); // pass the destination length-field address + emitter.instruction("call __rt_get_string_context_option"); // populate this optional string field when present + } + + // -- resolve verify_peer with PHP scalar truthiness (default true) -- + emitter.instruction("mov QWORD PTR [rbp - 184], 1"); // default verify_peer to true for TLS clients + abi::emit_symbol_address(emitter, "rdi", "_ssl_key_str"); // pass the TLS context wrapper name + emitter.instruction("mov rsi, 3"); // pass strlen("ssl") + abi::emit_symbol_address(emitter, "rdx", "_ssl_verify_peer_key_str"); // pass the verify_peer option name + emitter.instruction("mov rcx, 11"); // pass strlen("verify_peer") + emitter.instruction("lea r8, [rbp - 184]"); // pass the verify_peer output address + emitter.instruction("call __rt_get_bool_context_option"); // accept bool/int/string PHP truthiness + emitter.instruction("mov r10d, DWORD PTR [rbp - 184]"); // load the normalized verify_peer value + emitter.instruction("and r10d, 1"); // constrain the policy contribution to one bit + emitter.instruction("mov eax, DWORD PTR [rbp - 172]"); // load the current TLS policy bitmask + emitter.instruction("and eax, -2"); // clear the previous verify_peer bit + emitter.instruction("or eax, r10d"); // merge the resolved verify_peer bit + emitter.instruction("mov DWORD PTR [rbp - 172], eax"); // persist the updated TLS policy + + // -- resolve verify_peer_name independently (default true) -- + emitter.instruction("mov QWORD PTR [rbp - 184], 1"); // default verify_peer_name to true for TLS clients + abi::emit_symbol_address(emitter, "rdi", "_ssl_key_str"); // pass the TLS context wrapper name + emitter.instruction("mov rsi, 3"); // pass strlen("ssl") + abi::emit_symbol_address(emitter, "rdx", "_ssl_verify_peer_name_key_str"); // pass the verify_peer_name option name + emitter.instruction("mov rcx, 16"); // pass strlen("verify_peer_name") + emitter.instruction("lea r8, [rbp - 184]"); // pass the verify_peer_name output address + emitter.instruction("call __rt_get_bool_context_option"); // accept bool/int/string PHP truthiness + emitter.instruction("mov r10d, DWORD PTR [rbp - 184]"); // load the normalized verify_peer_name value + emitter.instruction("and r10d, 1"); // constrain the policy contribution to one bit + emitter.instruction("shl r10d, 1"); // move the value into policy bit one + emitter.instruction("mov eax, DWORD PTR [rbp - 172]"); // load the current TLS policy bitmask + emitter.instruction("and eax, -3"); // clear the previous verify_peer_name bit + emitter.instruction("or eax, r10d"); // merge the resolved verify_peer_name bit + emitter.instruction("mov DWORD PTR [rbp - 172], eax"); // persist the updated TLS policy + + // -- resolve allow_self_signed independently (default false) -- + emitter.instruction("mov QWORD PTR [rbp - 184], 0"); // default allow_self_signed to false + abi::emit_symbol_address(emitter, "rdi", "_ssl_key_str"); // pass the TLS context wrapper name + emitter.instruction("mov rsi, 3"); // pass strlen("ssl") + abi::emit_symbol_address(emitter, "rdx", "_ssl_allow_self_signed_key_str"); // pass the allow_self_signed option name + emitter.instruction("mov rcx, 17"); // pass strlen("allow_self_signed") + emitter.instruction("lea r8, [rbp - 184]"); // pass the allow_self_signed output address + emitter.instruction("call __rt_get_bool_context_option"); // accept bool/int/string PHP truthiness + emitter.instruction("mov r10d, DWORD PTR [rbp - 184]"); // load the normalized allow_self_signed value + emitter.instruction("and r10d, 1"); // constrain the policy contribution to one bit + emitter.instruction("shl r10d, 2"); // move the value into policy bit two + emitter.instruction("mov eax, DWORD PTR [rbp - 172]"); // load the current TLS policy bitmask + emitter.instruction("and eax, -5"); // clear the previous allow_self_signed bit + emitter.instruction("or eax, r10d"); // merge the resolved allow_self_signed bit + emitter.instruction("mov DWORD PTR [rbp - 172], eax"); // persist the updated TLS policy + + // -- connect_with_options(host, host_len, port, &options) -- + abi::emit_load_symbol_to_reg(emitter, "r9", "_elephc_tls_connect_with_options_fn", 0); // load the policy-aware TLS connect entry emitter.instruction("test r9, r9"); // missing elephc-tls runtime means HTTPS open fails closed emitter.instruction("jz __rt_https_open_fail_x86"); // return a failed HTTPS stream when no TLS entry is available emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // arg 0 = host pointer emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // arg 1 = host length - emitter.instruction("mov rdx, QWORD PTR [rbp - 32]"); // arg 2 = port (rcx/r8 hold cafile args) - emitter.instruction("call r9"); // open the TLS session, rax = handle + emitter.instruction("mov rdx, QWORD PTR [rbp - 32]"); // arg 2 = connection port + emitter.instruction("lea rcx, [rbp - 176]"); // arg 3 = stable TLS options struct address + emitter.emit_published_bridge_call("r9"); // call the published TLS adapter, rax = handle emitter.instruction("cmp rax, 0"); // did the TLS handshake fail? emitter.instruction("jl __rt_https_open_fail_x86"); // negative handle means TLS connect failed emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the TLS session handle - // -- elephc_tls_write(handle, req_ptr, req_len) -- - emitter.instruction("mov rdi, rax"); // arg 0 = TLS handle for the write + // -- write the complete HTTP request, preserving partial TLS writes -- + emitter.instruction("mov QWORD PTR [rbp - 80], 0"); // request write offset starts at zero + emitter.label("__rt_https_open_write_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 80]"); // reload bytes already sent through TLS + emitter.instruction("cmp r10, QWORD PTR [rbp - 48]"); // has every request byte been written? + emitter.instruction("jge __rt_https_open_read_start_x86"); // begin reading only after a complete request write + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // arg 0 = TLS handle for the write emitter.instruction("mov rsi, QWORD PTR [rbp - 40]"); // arg 1 = request pointer - emitter.instruction("mov rdx, QWORD PTR [rbp - 48]"); // arg 2 = request length + emitter.instruction("add rsi, r10"); // advance past request bytes already sent + emitter.instruction("mov rdx, QWORD PTR [rbp - 48]"); // reload the full request length + emitter.instruction("sub rdx, r10"); // pass only the unwritten request suffix abi::emit_load_symbol_to_reg(emitter, "r9", "_elephc_tls_write_fn", 0); // load the elephc_tls_write entry pointer - emitter.instruction("call r9"); // send the HTTP request through TLS + emitter.emit_published_bridge_call("r9"); // send the request through the published TLS adapter + emitter.instruction("test rax, rax"); // TLS write must make forward progress + emitter.instruction("jle __rt_https_open_tls_failed_x86"); // errors, WouldBlock, TimedOut, and zero all fail this blocking wrapper + emitter.instruction("add QWORD PTR [rbp - 80], rax"); // retain progress for the next write iteration + emitter.instruction("jmp __rt_https_open_write_x86"); // continue until the complete request is sent // -- read the whole TLS-decrypted response into _https_resp_buf -- + emitter.label("__rt_https_open_read_start_x86"); emitter.instruction("mov QWORD PTR [rbp - 56], 0"); // accumulated response length = 0 emitter.label("__rt_https_open_read_x86"); emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // TLS handle for the read @@ -419,21 +421,29 @@ fn emit_https_linux_x86_64(emitter: &mut Emitter) { emitter.instruction(&format!("mov rdx, {}", HTTPS_RESP_BUF_SIZE)); // response buffer capacity emitter.instruction("sub rdx, QWORD PTR [rbp - 56]"); // remaining buffer capacity emitter.instruction("cmp rdx, 0"); // is the response buffer full? - emitter.instruction("jle __rt_https_open_read_done_x86"); // stop when no capacity remains + emitter.instruction("jle __rt_https_open_tls_failed_x86"); // fail instead of returning a truncated response body abi::emit_load_symbol_to_reg(emitter, "r9", "_elephc_tls_read_fn", 0); // load the elephc_tls_read entry pointer - emitter.instruction("call r9"); // read more decrypted bytes from the TLS session - emitter.instruction("cmp rax, 0"); // did the read hit EOF or fail? - emitter.instruction("jle __rt_https_open_read_done_x86"); // the server closed the TLS session + emitter.emit_published_bridge_call("r9"); // read through the published TLS adapter + emitter.instruction("test rax, rax"); // distinguish a clean EOF from every TLS error sentinel + emitter.instruction("js __rt_https_open_tls_failed_x86"); // terminal, WouldBlock, and TimedOut reads must not become EOF + emitter.instruction("jz __rt_https_open_read_done_x86"); // a zero-byte TLS read is the sole successful EOF emitter.instruction("mov r10, QWORD PTR [rbp - 56]"); // reload the accumulated response length emitter.instruction("add r10, rax"); // advance by the bytes just read emitter.instruction("mov QWORD PTR [rbp - 56], r10"); // store the updated response length emitter.instruction("jmp __rt_https_open_read_x86"); // continue reading the TLS response emitter.label("__rt_https_open_read_done_x86"); + emitter.instruction("mov QWORD PTR [rbp - 80], 0"); // mark clean TLS completion before closing the session + emitter.instruction("jmp __rt_https_open_tls_close_x86"); // release the TLS handle before materializing the stream + emitter.label("__rt_https_open_tls_failed_x86"); + emitter.instruction("mov QWORD PTR [rbp - 80], 1"); // retain failure state across TLS cleanup // -- elephc_tls_close(handle): send close_notify and drop the session -- + emitter.label("__rt_https_open_tls_close_x86"); emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // TLS handle for the close abi::emit_load_symbol_to_reg(emitter, "r9", "_elephc_tls_close_fn", 0); // load the elephc_tls_close entry pointer - emitter.instruction("call r9"); // shut the TLS session down cleanly + emitter.emit_published_bridge_call("r9"); // close through the published TLS adapter + emitter.instruction("cmp QWORD PTR [rbp - 80], 0"); // did request/response I/O fail before this cleanup? + emitter.instruction("jne __rt_https_open_fail_x86"); // never materialize a partial response after TLS I/O failed // -- scan for the CRLFCRLF that separates headers from the body -- abi::emit_symbol_address(emitter, "r8", "_https_resp_buf"); // response buffer base @@ -473,28 +483,155 @@ fn emit_https_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jl __rt_https_open_fail_x86"); // propagate the failure emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // save the temp-file descriptor - // -- write(temp, body, body length) -- - emitter.instruction("mov rdi, rax"); // temp-file descriptor for the write + // -- write the complete body into the temporary stream -- + emitter.instruction("mov QWORD PTR [rbp - 80], 0"); // temporary-file write offset starts at zero + emitter.label("__rt_https_open_body_write_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 80]"); // reload bytes already copied to the temporary file + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload the full response length + emitter.instruction("sub r11, QWORD PTR [rbp - 64]"); // derive the complete body length + emitter.instruction("cmp r10, r11"); // has the full response body reached the tempfile? + emitter.instruction("jge __rt_https_open_body_written_x86"); // continue only after every body byte was persisted + emitter.instruction("mov rdi, QWORD PTR [rbp - 72]"); // temp-file descriptor for the write abi::emit_symbol_address(emitter, "rsi", "_https_resp_buf"); // response buffer base - emitter.instruction("add rsi, QWORD PTR [rbp - 64]"); // body pointer = buffer + body start - emitter.instruction("mov rdx, QWORD PTR [rbp - 56]"); // response length - emitter.instruction("sub rdx, QWORD PTR [rbp - 64]"); // body length = response length - body start + emitter.instruction("add rsi, QWORD PTR [rbp - 64]"); // point at the beginning of the response body + emitter.instruction("add rsi, r10"); // advance past body bytes already copied + emitter.instruction("mov rdx, r11"); // reload the full body length + emitter.instruction("sub rdx, r10"); // pass the remaining body suffix emitter.instruction("call write"); // copy the body into the temp file + emitter.instruction("test rax, rax"); // tempfile writes must make forward progress + emitter.instruction("jle __rt_https_open_temp_fail_x86"); // close the tempfile instead of returning partial bytes + emitter.instruction("add QWORD PTR [rbp - 80], rax"); // retain progress for the next copy iteration + emitter.instruction("jmp __rt_https_open_body_write_x86"); // continue until the entire body is copied + emitter.label("__rt_https_open_body_written_x86"); // -- lseek(temp, 0, SEEK_SET): rewind so the stream reads from the start -- emitter.instruction("mov rdi, QWORD PTR [rbp - 72]"); // reload the 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 + emitter.instruction("test rax, rax"); // a negative seek result makes the temporary stream unusable + emitter.instruction("js __rt_https_open_temp_fail_x86"); // close the unreadable temporary stream before failing emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // return the rewound body descriptor - emitter.instruction("add rsp, 96"); // release the helper frame + emitter.instruction("add rsp, 192"); // release the helper frame emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the https:// stream descriptor + emitter.label("__rt_https_open_temp_fail_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 72]"); // close the temporary descriptor before reporting failure + emitter.instruction("call close"); // release the anonymous temporary stream after a failed copy or rewind + emitter.instruction("jmp __rt_https_open_fail_x86"); // reuse the common failure epilogue after cleanup + emitter.label("__rt_https_open_fail_x86"); emitter.instruction("mov rax, -1"); // -1 signals a failed https:// open - emitter.instruction("add rsp, 96"); // release the helper frame + emitter.instruction("add rsp, 192"); // release the helper frame emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the failure result } + +#[cfg(test)] +mod tests { + use crate::codegen_support::emit::Emitter; + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::*; + + /// Verifies every Windows x86_64 HTTPS call uses the published TLS adapter. + /// + /// The pointer slots expose compiler-runtime-ABI adapters, so the four + /// connect/write/read/close sites must stay as direct calls through `r9` + /// instead of applying the native C-ABI shim a second time. + #[test] + fn test_windows_x86_64_https_native_bridge_calls_use_shim() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_https(&mut emitter); + let asm = emitter.output(); + + assert_eq!(asm.matches("call r9").count(), 4, "all four TLS calls must use the published adapters"); + assert!(!asm.contains("call r11"), "published adapters must not be wrapped as native C callbacks"); + assert!(!asm.contains("mov r11, r9"), "published adapters already expose the runtime ABI"); + } + + /// Verifies Linux HTTPS keeps direct calls through published bridge slots. + #[test] + fn test_linux_x86_64_https_calls_stay_bare() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_https(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains(" call r9\n"), "linux keeps the bare published-slot call"); + assert!(!asm.contains("call r11"), "linux must not emit the windows native-bridge shim"); + } + + /// Pins the policy-aware options layout and combinable context lookups on both backends. + #[test] + fn https_builds_policy_options_without_legacy_dispatch() { + for target in [ + Target::new(Platform::MacOS, Arch::AArch64), + Target::new(Platform::Linux, Arch::X86_64), + ] { + let mut emitter = Emitter::new(target); + emit_https(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains("_elephc_tls_connect_with_options_fn")); + assert!(!asm.contains("_elephc_tls_connect_insecure_fn")); + assert!(!asm.contains("_elephc_tls_connect_cafile_fn")); + assert!(!asm.contains("_elephc_tls_connect_capath_fn")); + assert!(!asm.contains("_elephc_tls_connect_peer_name_fn")); + assert_eq!(asm.matches("__rt_get_bool_context_option").count(), 3); + assert_eq!(asm.matches("__rt_get_string_context_option").count(), 5); + for key in [ + "_ssl_peer_name_key_str", + "_ssl_cafile_key_str", + "_ssl_capath_key_str", + "_ssl_local_cert_key_str", + "_ssl_local_pk_key_str", + ] { + assert!(asm.contains(key), "{key} must populate the shared options struct"); + } + } + } + + /// Pins every byte offset in the stable 88-byte TLS client options ABI. + #[test] + fn https_tls_options_offsets_match_bridge_abi() { + let mut arm = Emitter::new(Target::new(Platform::MacOS, Arch::AArch64)); + emit_https(&mut arm); + let arm_asm = arm.output(); + for offset in [48, 52, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128] { + assert!(arm_asm.contains(&format!("[sp, #{offset}]"))); + } + + let mut x86 = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_https(&mut x86); + let x86_asm = x86.output(); + for offset in [176, 172, 168, 160, 152, 144, 136, 128, 120, 112, 104, 96] { + assert!(x86_asm.contains(&format!("[rbp - {offset}]"))); + } + } + + /// Verifies HTTPS only returns a tempfile after complete request and body I/O. + #[test] + fn https_rejects_tls_errors_and_partial_tempfiles() { + let mut arm = Emitter::new(Target::new(Platform::MacOS, Arch::AArch64)); + emit_https(&mut arm); + let arm_asm = arm.output(); + assert!(arm_asm.contains("__rt_https_open_write:")); + assert!(arm_asm.contains("__rt_https_open_tls_failed:")); + assert!(arm_asm.contains("b.lt __rt_https_open_tls_failed")); + assert!(arm_asm.contains("__rt_https_open_body_write:")); + assert!(arm_asm.contains("__rt_https_open_temp_fail:")); + assert!(!arm_asm.contains("b.le __rt_https_open_read_done")); + + let mut x86 = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_https(&mut x86); + let x86_asm = x86.output(); + assert!(x86_asm.contains("__rt_https_open_write_x86:")); + assert!(x86_asm.contains("__rt_https_open_tls_failed_x86:")); + assert!(x86_asm.contains("js __rt_https_open_tls_failed_x86")); + assert!(x86_asm.contains("__rt_https_open_body_write_x86:")); + assert!(x86_asm.contains("__rt_https_open_temp_fail_x86:")); + assert!(!x86_asm.contains("jle __rt_https_open_read_done_x86")); + } +} diff --git a/src/codegen_support/runtime/io/inet6_pton.rs b/src/codegen_support/runtime/io/inet6_pton.rs index cc95f6f5b0..3ee3ae0921 100644 --- a/src/codegen_support/runtime/io/inet6_pton.rs +++ b/src/codegen_support/runtime/io/inet6_pton.rs @@ -82,7 +82,7 @@ fn emit_inet6_pton_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rsi, rax"); // c_str into argument 1 (src) emitter.instruction("mov rdx, QWORD PTR [rbp - 8]"); // reload the out buffer pointer into argument 2 (dst) emitter.instruction(&format!("mov edi, {}", af_inet6)); // family: AF_INET6 (30 on macOS, 10 on Linux) - emitter.instruction("call inet_pton"); // rax = 1 success, 0 fail, -1 EAFNOSUPPORT + emitter.emit_call_c("inet_pton"); // rax = 1 success, 0 fail, -1 EAFNOSUPPORT // -- collapse libc result to 0/1 (any non-positive return means fail) -- emitter.instruction("cmp eax, 1"); // did libc report exactly one successful conversion? diff --git a/src/codegen_support/runtime/io/mod.rs b/src/codegen_support/runtime/io/mod.rs index 00a101cf22..e49bd9790b 100644 --- a/src/codegen_support/runtime/io/mod.rs +++ b/src/codegen_support/runtime/io/mod.rs @@ -28,6 +28,7 @@ mod fread; mod fwrite; mod http_response; mod php_input; +mod php_temp_dir; mod stdout_write; mod ob_buffer; mod ob_handler; @@ -78,6 +79,7 @@ mod stream_wrapper_unregister; mod http_build_request; mod stream_context_get_ssl_peer_name; mod apply_socket_opts; +mod stream_context_get_bool_option; mod stream_context_get_int_option; mod stream_context_get_string_option; mod stream_context_set_option_4; @@ -89,6 +91,11 @@ mod stream_socket_accept; mod stream_socket_client; mod pclose; mod popen; +mod proc_close; +mod proc_open; +mod proc_open_marshal; +mod proc_pipe_registry; +mod proc_status; mod opendir; mod readdir; mod closedir; @@ -109,11 +116,13 @@ mod stream_set_timeout; mod streams_ext; mod symlink; mod tempnam; +mod tls_session_table; mod path_is_wrapper; mod readfile_wrapper; mod user_filter; mod user_filter_brigade; mod stash_connect_host; +mod tls_scheme; mod touch_meta_array; mod user_wrapper; mod user_wrapper_cast; @@ -146,6 +155,7 @@ pub(crate) use fwrite::emit_fwrite; pub(crate) use http_response::{emit_header, emit_http_response_code}; pub(crate) use php_input::emit_php_input; pub(crate) use stdout_write::emit_stdout_write; +pub(crate) use php_temp_dir::emit_php_temp_dir; pub(crate) use phar_read::emit_phar_read; pub(crate) use phar_write::emit_phar_write; pub(crate) use fs::emit_fs; @@ -194,6 +204,7 @@ pub(crate) use stream_context_get_ssl_peer_name::emit_get_ssl_peer_name; pub(crate) use apply_socket_opts::{ emit_apply_socket_bindto, emit_apply_socket_client_opts, emit_apply_socket_server_opts, }; +pub(crate) use stream_context_get_bool_option::emit_get_bool_context_option; pub(crate) use stream_context_get_int_option::emit_get_int_context_option; pub(crate) use stream_context_get_string_option::emit_get_string_context_option; pub(crate) use stream_context_set_option_4::emit_stream_context_set_option_4; @@ -205,6 +216,11 @@ pub(crate) use stream_socket_accept::emit_stream_socket_accept; pub(crate) use stream_socket_client::emit_stream_socket_client; pub(crate) use pclose::emit_pclose; pub(crate) use popen::emit_popen; +pub(crate) use proc_close::emit_proc_close; +pub(crate) use proc_pipe_registry::emit_proc_pipe_registry; +pub(crate) use proc_status::emit_proc_status; +pub(crate) use proc_open::emit_proc_open; +pub(crate) use proc_open_marshal::emit_proc_open_marshalling; pub(crate) use opendir::emit_opendir; pub(crate) use readdir::emit_readdir; pub(crate) use closedir::emit_closedir; @@ -225,6 +241,7 @@ pub(crate) use stream_set_timeout::emit_stream_set_timeout; pub(crate) use streams_ext::emit_streams_ext; pub(crate) use symlink::emit_symlink; pub(crate) use tempnam::emit_tempnam; +pub(crate) use tls_session_table::emit_tls_session_table; pub(crate) use user_filter::{ emit_apply_user_stream_filter, emit_resolve_user_filter_id, emit_stream_filter_attach_user, emit_stream_filter_register, @@ -246,6 +263,8 @@ pub(crate) use user_wrapper_dir::{ }; pub(crate) use user_wrapper_path_op::{emit_user_wrapper_path_op, emit_user_wrapper_rename}; pub(crate) use stash_connect_host::emit_stash_connect_host; +/// Emit the TLS-transport scheme predicate helper. +pub(crate) use tls_scheme::emit_addr_tls_crypto_method; pub(crate) use notification::emit_fire_notification; pub(crate) use touch_meta_array::emit_touch_meta_array; pub(crate) use user_wrapper_set_option::emit_user_wrapper_set_option; diff --git a/src/codegen_support/runtime/io/modify_x86_64.rs b/src/codegen_support/runtime/io/modify_x86_64.rs index ef5115de11..8f7f1c9757 100644 --- a/src/codegen_support/runtime/io/modify_x86_64.rs +++ b/src/codegen_support/runtime/io/modify_x86_64.rs @@ -61,7 +61,7 @@ pub(super) fn emit_modify_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rdi, rax"); // first libc chown arg = path emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // second arg = uid emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // third arg = gid - emitter.instruction("call chown"); // libc chown(path, uid, gid) + emitter.emit_call_c("chown"); // libc chown(path, uid, gid) — false on Windows emitter.instruction("cmp eax, 0"); // did libc chown() return success as a C int? emitter.instruction("sete al"); // boolean byte emitter.instruction("movzx rax, al"); // widen @@ -82,7 +82,7 @@ pub(super) fn emit_modify_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rdi, rax"); // first libc lchown arg = path emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // second arg = uid emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // third arg = gid - emitter.instruction("call lchown"); // libc lchown(path, uid, gid) without following symlinks + emitter.emit_call_c("lchown"); // libc lchown(path, uid, gid) — false on Windows emitter.instruction("cmp eax, 0"); // did libc lchown() return success as a C int? emitter.instruction("sete al"); // boolean byte emitter.instruction("movzx rax, al"); // widen @@ -112,7 +112,7 @@ pub(super) fn emit_modify_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // first chown arg = C path emitter.instruction("mov esi, eax"); // second chown arg = resolved uid emitter.instruction("mov rdx, -1"); // gid = -1 (leave group unchanged) - emitter.instruction("call chown"); // libc chown(path, uid, -1) + emitter.emit_call_c("chown"); // libc chown(path, uid, -1) — false on Windows emitter.instruction("cmp eax, 0"); // did libc chown() return success as a C int? emitter.instruction("sete al"); // boolean byte emitter.instruction("movzx rax, al"); // widen @@ -146,7 +146,7 @@ pub(super) fn emit_modify_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // first lchown arg = C path emitter.instruction("mov esi, eax"); // second lchown arg = resolved uid emitter.instruction("mov rdx, -1"); // gid = -1 (leave group unchanged) - emitter.instruction("call lchown"); // libc lchown(path, uid, -1) without following symlinks + emitter.emit_call_c("lchown"); // libc lchown(path, uid, -1) — false on Windows emitter.instruction("cmp eax, 0"); // did libc lchown() return success as a C int? emitter.instruction("sete al"); // boolean byte emitter.instruction("movzx rax, al"); // widen @@ -180,7 +180,7 @@ pub(super) fn emit_modify_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // first chown arg = C path emitter.instruction("mov rsi, -1"); // uid = -1 (leave owner unchanged) emitter.instruction("mov edx, eax"); // third chown arg = resolved gid - emitter.instruction("call chown"); // libc chown(path, -1, gid) + emitter.emit_call_c("chown"); // libc chown(path, -1, gid) — false on Windows emitter.instruction("cmp eax, 0"); // did libc chown() return success as a C int? emitter.instruction("sete al"); // boolean byte emitter.instruction("movzx rax, al"); // widen @@ -214,7 +214,7 @@ pub(super) fn emit_modify_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // first lchown arg = C path emitter.instruction("mov rsi, -1"); // uid = -1 (leave owner unchanged) emitter.instruction("mov edx, eax"); // third lchown arg = resolved gid - emitter.instruction("call lchown"); // libc lchown(path, -1, gid) without following symlinks + emitter.emit_call_c("lchown"); // libc lchown(path, -1, gid) — false on Windows emitter.instruction("cmp eax, 0"); // did libc lchown() return success as a C int? emitter.instruction("sete al"); // boolean byte emitter.instruction("movzx rax, al"); // widen @@ -274,7 +274,7 @@ pub(super) fn emit_modify_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rbp, rsp"); // establish frame emitter.instruction("mov rdi, rax"); // fd if emitter.platform == crate::codegen_support::platform::Platform::Linux { - emitter.instruction("call fdatasync"); // libc fdatasync(fd) on Linux + emitter.emit_call_c("fdatasync"); // libc fdatasync(fd) on Linux } else { emitter.instruction("call fsync"); // Darwin fallback } diff --git a/src/codegen_support/runtime/io/notification.rs b/src/codegen_support/runtime/io/notification.rs index ef4cfc18df..f4570e62d5 100644 --- a/src/codegen_support/runtime/io/notification.rs +++ b/src/codegen_support/runtime/io/notification.rs @@ -304,14 +304,14 @@ fn emit_fire_notification_linux_x86_64(emitter: &mut Emitter) { emitter.instruction(&format!("mov r10, QWORD PTR [rdi + {}]", CALLABLE_DESC_INVOKER_OFFSET)); // load the per-callable invoker function pointer emitter.instruction("test r10, r10"); // no invoker (shouldn't happen)? emitter.instruction("jz __rt_hfn_free_x86"); // just release the args when absent - emitter.instruction("call r10"); // invoke notification($code, …) → boxed Mixed result in rax - abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the ignored boxed return value (pointer already in rax) + emitter.emit_platform_callback_call("r10", 2); + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the ignored boxed return value (pointer already in rax) emitter.label("__rt_hfn_free_x86"); emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // boxed Mixed argument cell - abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the cell (drops the array ref taken by boxing) + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the cell (drops the array ref taken by boxing) emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // raw args array pointer - abi::emit_call_label(emitter, "__rt_decref_any"); // release the args array and its boxed elements + abi::emit_call_label(emitter, "__rt_decref_any"); // release the args array and its boxed elements emitter.label("__rt_hfn_done_x86"); emitter.instruction("add rsp, 80"); // release the shim frame diff --git a/src/codegen_support/runtime/io/ob_handler.rs b/src/codegen_support/runtime/io/ob_handler.rs index ec02101a6c..f8405cd786 100644 --- a/src/codegen_support/runtime/io/ob_handler.rs +++ b/src/codegen_support/runtime/io/ob_handler.rs @@ -389,7 +389,7 @@ fn emit_ob_invoke_descriptor_x86_64(emitter: &mut Emitter) { emitter.instruction("jz __rt_ob_invoke_desc_missing_x86"); // no invoker recorded — pass the raw bytes through emitter.instruction("mov rdi, r10"); // invoker arg0 = the callable descriptor emitter.instruction("mov rsi, QWORD PTR [rbp - 40]"); // invoker arg1 = the boxed argument container - emitter.instruction("call r11"); // run the handler → boxed Mixed result + emitter.emit_platform_callback_call("r11", 2); // run the generated invoker with the target callback ABI // -- map the result BEFORE releasing the container (aliasing safety) -- emitter.instruction("call __rt_ob_result_to_bytes"); // map the result cell to the replacement triple emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // save the replaced flag @@ -451,7 +451,7 @@ fn emit_ob_eval_trampoline_x86_64(emitter: &mut Emitter) { emitter.instruction("mov r9, QWORD PTR [r9]"); // load the installed magician hook emitter.instruction("test r9, r9"); // is an eval hook installed? emitter.instruction("jz __rt_ob_eval_trampoline_none_x86"); // no hook installed — pass the raw bytes through - emitter.instruction("call r9"); // hook(id, buf, len, phase) → Mixed result cell + emitter.emit_native_bridge_call("r9", 4); // call the Rust hook with the target C ABI emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("jmp __rt_ob_result_to_bytes"); // tail-map the result cell to the replacement triple emitter.label("__rt_ob_eval_trampoline_none_x86"); @@ -615,4 +615,46 @@ mod tests { assert!(linux_x86.contains("_ob_in_handler")); assert!(linux_x86.contains("call __rt_mixed_cast_string")); } + + /// Verifies Windows remaps descriptor invoker arguments into MSx64 registers. + #[test] + fn windows_descriptor_invoker_uses_platform_callback_abi() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_ob_invoke_descriptor(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains("sub rsp, 32")); + assert!(asm.contains("mov rdx, rsi")); + assert!(asm.contains("mov rcx, rdi")); + assert!(asm.contains("call r11")); + assert!(asm.contains("add rsp, 32")); + } + + /// Verifies Windows calls the installed Rust eval hook through the native C ABI. + #[test] + fn windows_eval_hook_uses_native_bridge_abi() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_ob_eval_trampoline(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains("mov r11, r9")); + assert!(asm.contains("sub rsp, 32")); + assert!(asm.contains("mov r9, rcx")); + assert!(asm.contains("mov r8, rdx")); + assert!(asm.contains("mov rdx, rsi")); + assert!(asm.contains("mov rcx, rdi")); + assert!(asm.contains("call r11")); + assert!(asm.contains("add rsp, 32")); + } + + /// Verifies non-Windows x86_64 keeps bare internal and callback calls. + #[test] + fn linux_x86_64_handler_calls_stay_bare() { + let asm = render(Platform::Linux, Arch::X86_64); + + assert!(asm.contains("call r10")); + assert!(asm.contains("call r11")); + assert!(asm.contains("call r9")); + assert!(!asm.contains("mov r11, r9")); + } } diff --git a/src/codegen_support/runtime/io/opendir.rs b/src/codegen_support/runtime/io/opendir.rs index d646b2e8bb..4ec51e1ff6 100644 --- a/src/codegen_support/runtime/io/opendir.rs +++ b/src/codegen_support/runtime/io/opendir.rs @@ -10,6 +10,10 @@ //! becomes the PHP directory stream resource value. //! - The `DIR*` is recorded in the `_dir_handles` table keyed by descriptor so //! `readdir()`, `rewinddir()`, and `closedir()` can hand it back to libc. +//! - The x86_64 `opendir`/`dirfd` call sites route through `Emitter::emit_call_c`. +//! On Windows, `__rt_sys_opendir` enumerates through `FindFirstFileExW` and +//! `dirfd` mints a small CRT descriptor used as the `_dir_handles` table key. +//! Other targets continue to use their libc implementations. use crate::codegen_support::{abi, emit::Emitter, platform::Arch}; @@ -150,7 +154,9 @@ fn emit_opendir_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jne __rt_opendir_no_glob_x86"); // not the glob scheme emitter.instruction("jmp __rt_opendir_glob"); // glob:// path: tail-call into the synthetic helper emitter.label("__rt_opendir_no_glob_x86"); + emitter.instruction("jmp __rt_opendir_native_x86"); // enter the independently unwindable native helper + emitter.label_global("__rt_opendir_native_x86"); emitter.instruction("push rbp"); // preserve the caller frame pointer emitter.instruction("mov rbp, rsp"); // establish the helper frame pointer emitter.instruction("sub rsp, 16"); // reserve a spill slot for the DIR* handle @@ -160,14 +166,14 @@ fn emit_opendir_linux_x86_64(emitter: &mut Emitter) { // -- open the directory stream -- emitter.instruction("mov rdi, rax"); // C-string path argument for opendir - emitter.bl_c("opendir"); + emitter.emit_call_c("opendir"); // Windows: enumerate through the FindFirstFileExW-backed shim emitter.instruction("test rax, rax"); // a NULL DIR* means opendir failed emitter.instruction("jz __rt_opendir_fail_x86"); // bail out on an opendir failure emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the DIR* across the dirfd call // -- recover the underlying descriptor with dirfd -- emitter.instruction("mov rdi, rax"); // DIR* argument for dirfd - emitter.bl_c("dirfd"); + emitter.emit_call_c("dirfd"); // Windows: mint a small CRT descriptor for the DIR state table // -- record the DIR* in the fd->DIR* table for readdir/closedir -- abi::emit_symbol_address(emitter, "r10", "_dir_handles"); // base of the fd->DIR* table diff --git a/src/codegen_support/runtime/io/opendir_glob.rs b/src/codegen_support/runtime/io/opendir_glob.rs index 7502b64a59..352781e2ec 100644 --- a/src/codegen_support/runtime/io/opendir_glob.rs +++ b/src/codegen_support/runtime/io/opendir_glob.rs @@ -86,7 +86,7 @@ pub fn emit_opendir_glob(emitter: &mut Emitter) { // -- dup(2) to mint a fresh fd we can hand out as the PHP resource value -- emitter.instruction("mov x0, #2"); // duplicate stderr (always available) emitter.bl_c("dup"); // x0 = new fd (-1 on failure) - emitter.instruction("cmp x0, #0"); // did dup fail? + emitter.instruction("cmp w0, #0"); // dup returns a C int: a 64-bit compare reads -1 as 0xffffffff and never trips this branch emitter.instruction("b.lt __rt_opendir_glob_fail"); // dup failed → bail emitter.instruction("cmp x0, #255"); // out-of-range for the 256-slot table? emitter.instruction("b.gt __rt_opendir_glob_fail"); // can't register this fd @@ -152,7 +152,7 @@ fn emit_opendir_glob_linux_x86_64(emitter: &mut Emitter) { // -- dup(2) to mint a fresh fd we can hand out as the PHP resource value -- emitter.instruction("mov edi, 2"); // duplicate stderr (always available) - emitter.instruction("call dup"); // rax = new fd (-1 on failure) + emitter.emit_call_c("dup"); // rax = new fd (-1 on failure) emitter.instruction("test rax, rax"); // did dup fail? emitter.instruction("js __rt_opendir_glob_fail_x86"); // negative → bail emitter.instruction("cmp rax, 255"); // out-of-range for the 256-slot table? diff --git a/src/codegen_support/runtime/io/phar_read.rs b/src/codegen_support/runtime/io/phar_read.rs index 88d5987422..97adaf3213 100644 --- a/src/codegen_support/runtime/io/phar_read.rs +++ b/src/codegen_support/runtime/io/phar_read.rs @@ -524,7 +524,7 @@ fn emit_phar_read_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("test r9, r9"); // was the bridge reader published? emitter.instruction("jz __rt_phar_read_asm_fallback_x86"); // use the assembly reader when no bridge was published abi::emit_symbol_address(emitter, "rdx", "_phar_extract_len"); // pass output-length scratch to the bridge - emitter.instruction("call r9"); // elephc_phar_extract_url(url_ptr, url_len, &len) + emitter.emit_native_bridge_call("r9", 3); // elephc_phar_extract_url(url_ptr, url_len, &len) emitter.instruction("test rax, rax"); // did the bridge find archive bytes? emitter.instruction("jz __rt_phar_read_fail_x86"); // bridge miss means archive or entry was not readable emitter.instruction("mov rdi, rax"); // pass extracted bytes to data_stream @@ -809,13 +809,14 @@ fn emit_phar_read_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("add rsp, 32"); // release the helper frame emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the failure (boxed false) - emitter.label("__rt_fgc_phar_plain_x86"); + emitter.label_global("__rt_fgc_phar_plain_x86"); emitter.instruction("jmp __rt_file_get_contents"); // tail-call the generic reader (args intact) } /// Emits x86_64 helpers that decompress PHAR entry payloads through published /// zlib/libbz2 function-pointer slots instead of direct runtime references. fn emit_phar_decompress_helpers_x86_64(emitter: &mut Emitter) { + let zstream = crate::codegen_support::stream_filters::zlib::z_stream_layout(emitter.target); emitter.blank(); emitter.comment("--- runtime: phar decompress helpers ---"); @@ -853,7 +854,7 @@ fn emit_phar_decompress_helpers_x86_64(emitter: &mut Emitter) { // -- zero the z_stream before handing it to zlib -- emitter.instruction("xor r9, r9"); // z_stream byte clear index emitter.label("__rt_phar_inflate_zero_x86"); - emitter.instruction("cmp r9, 112"); // cleared the whole z_stream struct? + emitter.instruction(&format!("cmp r9, {}", zstream.size)); // cleared the whole target-layout z_stream struct? emitter.instruction("jge __rt_phar_inflate_zeroed_x86"); // the z_stream is ready for initialization emitter.instruction("mov BYTE PTR [rsp + r9], 0"); // zero one z_stream byte emitter.instruction("inc r9"); // advance the clear index @@ -864,9 +865,9 @@ fn emit_phar_decompress_helpers_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rdi, rsp"); // arg 0 = z_stream pointer emitter.instruction("mov esi, -15"); // arg 1 = raw-DEFLATE window bits abi::emit_symbol_address(emitter, "rdx", "_zlib_version"); // arg 2 = zlib version string - emitter.instruction("mov ecx, 112"); // arg 3 = sizeof(z_stream) + emitter.instruction(&format!("mov ecx, {}", zstream.size)); // arg 3 = target sizeof(z_stream) abi::emit_load_symbol_to_reg(emitter, "r9", "_phar_zlib_inflate_init2_fn", 0); // reload the published inflateInit2_ pointer - emitter.instruction("call r9"); // initialize the raw-inflate stream + emitter.emit_native_bridge_call("r9", 4); // initialize the raw-inflate stream emitter.instruction("test eax, eax"); // did zlib initialize successfully? emitter.instruction("jnz __rt_phar_inflate_fail_x86"); // failed initialization makes the entry unreadable @@ -876,21 +877,25 @@ fn emit_phar_decompress_helpers_x86_64(emitter: &mut Emitter) { emitter.instruction("mov r9, QWORD PTR [rsp + 120]"); // reload compressed source length emitter.instruction("mov DWORD PTR [rsp + 8], r9d"); // z_stream.avail_in = source length emitter.instruction("mov r9, QWORD PTR [rsp + 136]"); // reload destination buffer pointer - emitter.instruction("mov QWORD PTR [rsp + 24], r9"); // z_stream.next_out = destination buffer + emitter.instruction(&format!("mov QWORD PTR [rsp + {}], r9", zstream.next_out)); // z_stream.next_out = destination buffer emitter.instruction("mov r9, QWORD PTR [rsp + 128]"); // reload expected decompressed length - emitter.instruction("mov DWORD PTR [rsp + 32], r9d"); // z_stream.avail_out = exact output capacity + emitter.instruction(&format!("mov DWORD PTR [rsp + {}], r9d", zstream.avail_out)); // z_stream.avail_out = exact output capacity // -- inflate the whole entry in one Z_FINISH pass, then end the stream -- emitter.instruction("mov rdi, rsp"); // arg 0 = z_stream pointer emitter.instruction("mov esi, 4"); // arg 1 = Z_FINISH abi::emit_load_symbol_to_reg(emitter, "r9", "_phar_zlib_inflate_fn", 0); // reload the published inflate pointer - emitter.instruction("call r9"); // inflate the compressed PHAR entry payload + emitter.emit_native_bridge_call("r9", 2); // inflate the compressed PHAR entry payload emitter.instruction("mov QWORD PTR [rsp + 144], rax"); // save the zlib status code - emitter.instruction("mov r9, QWORD PTR [rsp + 40]"); // z_stream.total_out = inflated length + if emitter.target.platform == crate::codegen_support::platform::Platform::Windows { + emitter.instruction(&format!("mov r9d, DWORD PTR [rsp + {}]", zstream.total_out)); // LLP64 z_stream.total_out = inflated length + } else { + emitter.instruction(&format!("mov r9, QWORD PTR [rsp + {}]", zstream.total_out)); // LP64 z_stream.total_out = inflated length + } emitter.instruction("mov QWORD PTR [rsp + 152], r9"); // save the actual inflated byte count emitter.instruction("mov rdi, rsp"); // arg 0 = z_stream pointer abi::emit_load_symbol_to_reg(emitter, "r9", "_phar_zlib_inflate_end_fn", 0); // reload the published inflateEnd pointer - emitter.instruction("call r9"); // release zlib's internal inflate state + emitter.emit_native_bridge_call("r9", 1); // release zlib's internal inflate state emitter.instruction("cmp QWORD PTR [rsp + 144], 1"); // did inflate reach Z_STREAM_END? emitter.instruction("jne __rt_phar_inflate_fail_x86"); // partial or failed inflate makes the entry unreadable @@ -941,7 +946,7 @@ fn emit_phar_decompress_helpers_x86_64(emitter: &mut Emitter) { emitter.instruction("xor r8d, r8d"); // arg 4 = small = false emitter.instruction("xor r9d, r9d"); // arg 5 = verbosity = 0 abi::emit_load_symbol_to_reg(emitter, "r10", "_phar_bz2_decompress_fn", 0); // reload the published BZ2 decompressor pointer - emitter.instruction("call r10"); // decompress the bzip2 PHAR entry payload + emitter.emit_native_bridge_call("r10", 6); // decompress the bzip2 PHAR entry payload emitter.instruction("test eax, eax"); // did libbz2 report success? emitter.instruction("jnz __rt_phar_bzip2_fail_x86"); // non-zero status makes the entry unreadable emitter.instruction("mov r9d, DWORD PTR [rsp + 32]"); // destLen now holds the decompressed byte count @@ -958,3 +963,46 @@ fn emit_phar_decompress_helpers_x86_64(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return decompressed bytes or null on failure } + +#[cfg(test)] +mod tests { + use crate::codegen_support::emit::Emitter; + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::*; + + /// Verifies every windows-x86_64 native-bridge fn-ptr call in the phar + /// read helpers (`elephc_phar_extract_url`, the zlib `inflateInit2_`/ + /// `inflate`/`inflateEnd` trio, and the libbz2 decompressor — all REAL + /// MSx64-ABI callees reached through pointer slots) routes through the F46 + /// `emit_native_bridge_call` shim instead of a bare `call r9`/`call r10`. + /// The shim is the ONLY producer of `call r11`, so an exact count of five + /// proves all five sites were converted; no bare register-indirect native + /// call may remain, or the MSx64 callee would read the SysV registers. + #[test] + fn test_windows_x86_64_phar_read_native_bridge_calls_use_shim() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_phar_read(&mut emitter); + let asm = emitter.output(); + + assert_eq!(asm.matches("call r11").count(), 5, "all five phar-read native-bridge calls must route through the F46 shim"); + assert!(!asm.contains("call r9\n"), "no bare call r9 may remain (MSx64 callee reading SysV registers)"); + assert!(!asm.contains("call r10\n"), "no bare call r10 may remain (MSx64 callee reading SysV registers)"); + assert!(asm.contains("mov r11, r9"), "expected an r9 fn-ptr relocated off the MSx64 arg registers"); + assert!(asm.contains("mov r11, r10"), "expected the r10 bzip2 fn-ptr relocated off the MSx64 arg registers"); + } + + /// Verifies linux-x86_64 phar-read emission stays byte-identical to before + /// the F46 shim: the MSx64 relocation/shadow staging is windows-x86_64-only, + /// so linux must keep the plain bare `call r9`/`call r10` and emit no shim. + #[test] + fn test_linux_x86_64_phar_read_calls_stay_bare() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_phar_read(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains(" call r9\n"), "linux keeps the byte-identical bare call r9"); + assert!(asm.contains(" call r10\n"), "linux keeps the byte-identical bare call r10"); + assert!(!asm.contains("call r11"), "linux must not emit the windows native-bridge shim"); + } +} diff --git a/src/codegen_support/runtime/io/phar_write.rs b/src/codegen_support/runtime/io/phar_write.rs index f0d0757661..de5eff5696 100644 --- a/src/codegen_support/runtime/io/phar_write.rs +++ b/src/codegen_support/runtime/io/phar_write.rs @@ -337,7 +337,7 @@ fn emit_phar_write_linux_x86_64(emitter: &mut Emitter) { abi::emit_load_symbol_to_reg(emitter, "rsi", "_phar_write_path_len", 0); // bridge arg 1 = archive path length abi::emit_load_symbol_to_reg(emitter, "rdx", "_phar_write_entry_ptr", 0); // bridge arg 2 = entry name pointer abi::emit_load_symbol_to_reg(emitter, "rcx", "_phar_write_entry_len", 0); // bridge arg 3 = entry name length - emitter.instruction("call r10"); // allocate a buffered PHAR write stream descriptor + emitter.emit_native_bridge_call("r10", 4); // allocate a buffered PHAR write stream descriptor emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the synthetic descriptor or failure sentinel emitter.label("__rt_phar_write_open_asm_x86"); @@ -367,7 +367,7 @@ fn emit_phar_write_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jz __rt_phar_write_open_url_asm_x86"); // no bridge published: use the assembly fallback emitter.instruction("push rbp"); // align the stack and preserve the caller frame pointer emitter.instruction("mov rbp, rsp"); // establish the bridge call frame - emitter.instruction("call r10"); // allocate a buffered runtime-URL PHAR write stream + emitter.emit_native_bridge_call("r10", 2); // allocate a buffered runtime-URL PHAR write stream emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the synthetic descriptor or failure sentinel emitter.label("__rt_phar_write_open_url_asm_x86"); @@ -392,7 +392,7 @@ fn emit_phar_write_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jz __rt_phar_write_append_asm_x86"); // no bridge published: append to the assembly fallback buffer emitter.instruction("push rbp"); // align the stack and preserve the caller frame pointer emitter.instruction("mov rbp, rsp"); // establish the bridge call frame - emitter.instruction("call r10"); // append to the selected buffered PHAR stream + emitter.emit_native_bridge_call("r10", 3); // append to the selected buffered PHAR stream emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return bytes consumed or the failure sentinel emitter.label("__rt_phar_write_append_asm_x86"); @@ -422,7 +422,7 @@ fn emit_phar_write_linux_x86_64(emitter: &mut Emitter) { abi::emit_load_symbol_to_reg(emitter, "r10", "_elephc_phar_stream_finalize_fn", 0); // load the optional buffered PHAR stream finalizer emitter.instruction("test r10, r10"); // was the stream finalizer bridge published? emitter.instruction("jz __rt_phar_write_finalize_asm_state_x86"); // no bridge published: use the assembly fallback state - emitter.instruction("call r10"); // flush and release the selected buffered PHAR stream + emitter.emit_native_bridge_call("r10", 1); // flush the selected descriptor through the platform C ABI emitter.instruction("jmp __rt_phar_write_finalize_return_x86"); // restore the frame and return the bridge result emitter.label("__rt_phar_write_finalize_asm_state_x86"); // -- compute the content length and the entry anchor -- @@ -447,7 +447,7 @@ fn emit_phar_write_linux_x86_64(emitter: &mut Emitter) { abi::emit_load_symbol_to_reg(emitter, "rdi", "_phar_write_url_ptr", 0); // bridge arg 0 = full phar:// URL pointer emitter.instruction("mov rdx, r8"); // bridge arg 2 = buffered entry payload pointer emitter.instruction("mov rcx, r9"); // bridge arg 3 = buffered entry payload length - emitter.instruction("call r10"); // insert/update the entry through the Rust PHAR URL bridge + emitter.emit_native_bridge_call("r10", 4); // insert/update the entry through the Rust PHAR URL bridge emitter.instruction("cmp rax, -1"); // did the bridge report usize::MAX failure? emitter.instruction("je __rt_phar_write_finalize_bridge_fail_x86"); // bridge failure makes fclose() false emitter.instruction("mov eax, 1"); // fclose() returns true after a successful bridge write @@ -463,7 +463,7 @@ fn emit_phar_write_linux_x86_64(emitter: &mut Emitter) { abi::emit_load_symbol_to_reg(emitter, "rsi", "_phar_write_path_len", 0); // bridge arg 1 = archive path length abi::emit_load_symbol_to_reg(emitter, "rdx", "_phar_write_entry_ptr", 0); // bridge arg 2 = entry name pointer abi::emit_load_symbol_to_reg(emitter, "rcx", "_phar_write_entry_len", 0); // bridge arg 3 = entry name length - emitter.instruction("call r10"); // insert/update the entry through the Rust PHAR bridge + emitter.emit_native_bridge_call("r10", 6); // insert/update the entry through the Rust PHAR bridge emitter.instruction("cmp rax, -1"); // did the bridge report usize::MAX failure? emitter.instruction("je __rt_phar_write_finalize_bridge_fail_x86"); // bridge failure makes fclose() false emitter.instruction("mov eax, 1"); // fclose() returns true after a successful bridge write @@ -495,7 +495,7 @@ fn emit_phar_write_linux_x86_64(emitter: &mut Emitter) { abi::emit_symbol_address(emitter, "r8", "_phar_write_out"); // elephc_crypto_hash out base = archive buffer base ... emitter.instruction("add r8, rcx"); // ... + length (raw 20 bytes past the data) abi::emit_load_symbol_to_reg(emitter, "r9", "_elephc_crypto_hash_fn", 0); // load the elephc-crypto hash entry pointer - emitter.instruction("call r9"); // compute the raw 20-byte SHA1 digest in place + emitter.emit_native_bridge_call("r9", 5); // compute the raw 20-byte SHA1 digest in place abi::emit_symbol_address(emitter, "r8", "_phar_write_len"); // buffer length slot emitter.instruction("mov rcx, QWORD PTR [r8]"); // reload length (the hash call clobbered caller-saved regs) abi::emit_symbol_address(emitter, "r9", "_phar_write_out"); // buffer base @@ -558,7 +558,7 @@ fn emit_file_put_contents_maybe_phar_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rsi, rdx"); // bridge arg 1 = full phar:// URL length emitter.instruction("mov rdx, r8"); // bridge arg 2 = payload pointer emitter.instruction("mov rcx, r9"); // bridge arg 3 = payload length - emitter.instruction("call r10"); // insert/update the entry through the Rust PHAR bridge + emitter.emit_native_bridge_call("r10", 4); // insert/update the entry through the Rust PHAR bridge emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the bridge byte count or -1 failure emitter.label("__rt_fpc_maybe_phar_fail_x86"); @@ -567,3 +567,47 @@ fn emit_file_put_contents_maybe_phar_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_fpc_maybe_phar_plain_x86"); emitter.instruction("jmp __rt_file_put_contents"); // tail-call the ordinary filesystem writer } + +#[cfg(test)] +mod tests { + use crate::codegen_support::emit::Emitter; + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::*; + + /// Verifies every windows-x86_64 native-bridge fn-ptr call in the phar + /// write helpers (the buffered stream open/append/finalize bridges, the + /// PHAR URL/entry writers, and the in-place SHA1 signature via + /// `elephc_crypto_hash` — all REAL MSx64-ABI callees reached through + /// pointer slots) routes through the F46 `emit_native_bridge_call` shim + /// instead of a bare `call r9`/`call r10`. The shim is the ONLY producer + /// of `call r11`, so an exact count of eight proves all eight sites were + /// converted; no bare register-indirect native call may remain, or the + /// MSx64 callee would read the SysV registers. + #[test] + fn test_windows_x86_64_phar_write_native_bridge_calls_use_shim() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_phar_write(&mut emitter); + let asm = emitter.output(); + + assert_eq!(asm.matches("call r11").count(), 8, "all eight phar-write native-bridge calls must route through the F46 shim"); + assert!(!asm.contains("call r9\n"), "no bare call r9 may remain (MSx64 callee reading SysV registers)"); + assert!(!asm.contains("call r10\n"), "no bare call r10 may remain (MSx64 callee reading SysV registers)"); + assert!(asm.contains("mov r11, r10"), "expected an r10 fn-ptr relocated off the MSx64 arg registers"); + assert!(asm.contains("mov r11, r9"), "expected the r9 crypto-hash fn-ptr relocated off the MSx64 arg registers"); + } + + /// Verifies linux-x86_64 phar-write emission stays byte-identical to before + /// the F46 shim: the MSx64 relocation/shadow staging is windows-x86_64-only, + /// so linux must keep the plain bare `call r9`/`call r10` and emit no shim. + #[test] + fn test_linux_x86_64_phar_write_calls_stay_bare() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_phar_write(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains(" call r9\n"), "linux keeps the byte-identical bare call r9"); + assert!(asm.contains(" call r10\n"), "linux keeps the byte-identical bare call r10"); + assert!(!asm.contains("call r11"), "linux must not emit the windows native-bridge shim"); + } +} diff --git a/src/codegen_support/runtime/io/php_input.rs b/src/codegen_support/runtime/io/php_input.rs index 3e6c71ff63..c35904a09b 100644 --- a/src/codegen_support/runtime/io/php_input.rs +++ b/src/codegen_support/runtime/io/php_input.rs @@ -40,9 +40,9 @@ pub fn emit_php_input(emitter: &mut Emitter, web: bool) { emitter.instruction("str x30, [sp]"); // preserve the caller return address before the nested helper calls if web { - emitter.bl_c("elephc_web_body_len"); // x0 = request body length (i64) from the bridge + emitter.emit_native_bridge_symbol_call("elephc_web_body_len", 0); // x0 = request body length (i64) from the bridge emitter.instruction("str x0, [sp, #8]"); // spill the length across the body-pointer call - emitter.bl_c("elephc_web_body_ptr"); // x0 = pointer to the request body bytes from the bridge + emitter.emit_native_bridge_symbol_call("elephc_web_body_ptr", 0); // x0 = pointer to the request body bytes from the bridge emitter.instruction("ldr x1, [sp, #8]"); // reload the body length into the ptr_read_string length argument emitter.instruction("bl __rt_ptr_read_string"); // copy the body into an owned PHP string (x1=ptr, x2=len out) } else { @@ -54,7 +54,7 @@ pub fn emit_php_input(emitter: &mut Emitter, web: bool) { emitter.instruction("ret"); // return to the caller } -/// Emits the x86_64 Linux variant of `__rt_php_input`. +/// Emits the x86_64 variant of `__rt_php_input`. fn emit_php_input_x86_64(emitter: &mut Emitter, web: bool) { emitter.blank(); emitter.comment("--- runtime: php_input (file_get_contents('php://input')) ---"); @@ -65,9 +65,9 @@ fn emit_php_input_x86_64(emitter: &mut Emitter, web: bool) { emitter.instruction("sub rsp, 16"); // reserve an aligned spill slot for the body length if web { - emitter.bl_c("elephc_web_body_len"); // rax = request body length (i64) from the bridge + emitter.emit_native_bridge_symbol_call("elephc_web_body_len", 0); // rax = request body length; Windows still reserves native shadow space emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // spill the length across the body-pointer call - emitter.bl_c("elephc_web_body_ptr"); // rax = pointer to the request body bytes from the bridge + emitter.emit_native_bridge_symbol_call("elephc_web_body_ptr", 0); // rax = body pointer; Windows still reserves native shadow space emitter.instruction("mov rdx, QWORD PTR [rbp - 8]"); // reload the body length into the ptr_read_string length argument emitter.instruction("call __rt_ptr_read_string"); // copy the body into an owned PHP string (rax=ptr, rdx=len out) } else { @@ -78,3 +78,44 @@ fn emit_php_input_x86_64(emitter: &mut Emitter, web: bool) { emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return to the caller } + +#[cfg(test)] +mod tests { + use super::*; + use crate::codegen_support::platform::{Platform, Target}; + + /// Verifies both zero-argument Windows body getters reserve and release + /// MSx64 shadow space even though they do not need register remapping. + #[test] + fn windows_web_body_getters_use_native_shadow_space() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_php_input(&mut emitter, true); + let asm = emitter.output(); + assert!(asm.contains("lea r11, [rip + elephc_web_body_len]")); + assert!(asm.contains("lea r11, [rip + elephc_web_body_ptr]")); + assert_eq!(asm.matches("sub rsp, 32").count(), 2, "{asm}"); + assert_eq!(asm.matches("call r11").count(), 2, "{asm}"); + assert_eq!(asm.matches("add rsp, 32").count(), 2, "{asm}"); + assert!(!asm.contains("call elephc_web_body_")); + } + + /// Verifies Linux x86 retains direct body-getter calls with no Windows shadow area. + #[test] + fn linux_x86_web_body_getters_remain_direct() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_php_input(&mut emitter, true); + let asm = emitter.output(); + assert!(asm.contains("call elephc_web_body_len")); + assert!(asm.contains("call elephc_web_body_ptr")); + assert!(!asm.contains("sub rsp, 32")); + } + + /// Verifies non-web Windows emission never references optional web bridge symbols. + #[test] + fn windows_non_web_input_does_not_reference_bridge() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_php_input(&mut emitter, false); + let asm = emitter.output(); + assert!(!asm.contains("elephc_web_")); + } +} diff --git a/src/codegen_support/runtime/io/php_temp_dir.rs b/src/codegen_support/runtime/io/php_temp_dir.rs new file mode 100644 index 0000000000..d64db9f654 --- /dev/null +++ b/src/codegen_support/runtime/io/php_temp_dir.rs @@ -0,0 +1,99 @@ +//! Purpose: +//! Emits the `__rt_php_temp_dir` runtime helper, which resolves php's temporary +//! directory the way `php_get_temporary_directory` does. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::io`. +//! +//! Key details: +//! - TMPDIR wins when it is set and non-empty, minus **exactly one** trailing slash, +//! so `/var/tmp/probe///` resolves to `/var/tmp/probe//` and a bare `/` resolves to +//! the empty string. Only an unset or empty TMPDIR falls back to `P_tmpdir`, which +//! php returns verbatim — trailing slash included on macOS. +//! - Both `sys_get_temp_dir()` and `tmpfile()` read the directory through here, so +//! the two can never disagree about where temporary files belong. + +use crate::codegen_support::{abi, emit::Emitter, platform::Arch}; + +/// Emits `__rt_php_temp_dir` for the current target. +/// +/// Takes no arguments. Returns php's temporary directory as a PHP string in +/// `abi::string_result_regs` (`x1`/`x2` on AArch64, `rax`/`rdx` on x86_64). The +/// pointer is borrowed — it addresses either the process environment block or a +/// literal in the data section — so callers that mutate the bytes must copy first. +pub fn emit_php_temp_dir(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: php temporary directory ---"); + emitter.label_global("__rt_php_temp_dir"); + match emitter.target.arch { + Arch::AArch64 => emit_aarch64(emitter), + Arch::X86_64 => emit_x86_64(emitter), + } +} + +/// Emits the AArch64 body of `__rt_php_temp_dir`. +fn emit_aarch64(emitter: &mut Emitter) { + emitter.instruction("sub sp, sp, #16"); // frame for the nested __rt_getenv call + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address + emitter.instruction("mov x29, sp"); // establish the helper frame pointer + abi::emit_symbol_address(emitter, "x1", "_tmpdir_env_name"); // name pointer for the environment lookup + emitter.instruction("mov x2, #6"); // "TMPDIR" is six bytes + emitter.instruction("bl __rt_getenv"); // x1 = value pointer, x2 = value length + emitter.instruction("cbz x2, __rt_php_temp_dir_fallback"); // unset or empty TMPDIR falls back to P_tmpdir + + // -- drop exactly one trailing slash, as php does -- + emitter.instruction("sub x9, x2, #1"); // index of the last byte + emitter.instruction("ldrb w10, [x1, x9]"); // load the last byte + emitter.instruction("cmp w10, #47"); // is it '/'? + emitter.instruction("b.ne __rt_php_temp_dir_done"); // nothing to strip + emitter.instruction("mov x2, x9"); // shorten the string by that one slash + emitter.instruction("b __rt_php_temp_dir_done"); // TMPDIR wins over the fallback + + // -- P_tmpdir fallback: measure the literal, which php returns verbatim -- + emitter.label("__rt_php_temp_dir_fallback"); + abi::emit_symbol_address(emitter, "x1", "_php_p_tmpdir"); // platform P_tmpdir literal + emitter.instruction("mov x2, #0"); // seed the length counter + emitter.label("__rt_php_temp_dir_measure"); + emitter.instruction("ldrb w9, [x1, x2]"); // load the next literal byte + emitter.instruction("cbz w9, __rt_php_temp_dir_done"); // stop at the C terminator + emitter.instruction("add x2, x2, #1"); // count this byte + emitter.instruction("b __rt_php_temp_dir_measure"); // keep measuring + + emitter.label("__rt_php_temp_dir_done"); + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the frame + emitter.instruction("ret"); // return ptr/len in the string result regs +} + +/// Emits the x86_64 body of `__rt_php_temp_dir`. +fn emit_x86_64(emitter: &mut Emitter) { + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish the helper frame pointer + abi::emit_symbol_address(emitter, "rax", "_tmpdir_env_name"); // name pointer for the environment lookup + emitter.instruction("mov rdx, 6"); // "TMPDIR" is six bytes + abi::emit_call_label(emitter, "__rt_getenv"); // rax = value pointer, rdx = value length + emitter.instruction("test rdx, rdx"); // did TMPDIR hold anything? + emitter.instruction("jz __rt_php_temp_dir_fallback_x86"); // unset or empty TMPDIR falls back to P_tmpdir + + // -- drop exactly one trailing slash, as php does -- + emitter.instruction("mov r9, rdx"); // copy the length to index the last byte + emitter.instruction("sub r9, 1"); // index of the last byte + emitter.instruction("cmp BYTE PTR [rax + r9], 47"); // is it '/'? + emitter.instruction("jne __rt_php_temp_dir_done_x86"); // nothing to strip + emitter.instruction("mov rdx, r9"); // shorten the string by that one slash + emitter.instruction("jmp __rt_php_temp_dir_done_x86"); // TMPDIR wins over the fallback + + // -- P_tmpdir fallback: measure the literal, which php returns verbatim -- + emitter.label("__rt_php_temp_dir_fallback_x86"); + abi::emit_symbol_address(emitter, "rax", "_php_p_tmpdir"); // platform P_tmpdir literal + emitter.instruction("mov rdx, 0"); // seed the length counter + emitter.label("__rt_php_temp_dir_measure_x86"); + emitter.instruction("cmp BYTE PTR [rax + rdx], 0"); // reached the C terminator? + emitter.instruction("je __rt_php_temp_dir_done_x86"); // the literal is fully measured + emitter.instruction("add rdx, 1"); // count this byte + emitter.instruction("jmp __rt_php_temp_dir_measure_x86"); // keep measuring + + emitter.label("__rt_php_temp_dir_done_x86"); + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return ptr/len in the string result regs +} diff --git a/src/codegen_support/runtime/io/principal_lookup.rs b/src/codegen_support/runtime/io/principal_lookup.rs index 3e10a8b225..2f526da9bc 100644 --- a/src/codegen_support/runtime/io/principal_lookup.rs +++ b/src/codegen_support/runtime/io/principal_lookup.rs @@ -7,6 +7,15 @@ //! //! Key details: //! - Helpers scan `/etc/passwd` or `/etc/group` with libc file I/O and return `-1` when absent. +//! - The x86_64 call sites route `fopen`/`fgets`/`fclose`/`strncmp`/`strchr`/ +//! `strtoul` through `Emitter::emit_call_c` (not a bare `instruction("call +//! ...")`): on windows-x86_64 each reaches a real `__rt_sys_*` msvcrt +//! arg-shuffle shim (`codegen_support::runtime::win32:: +//! emit_shim_msvcrt_passwd_lookup`) — all six are standard msvcrt symbols. +//! `/etc/passwd`/`/etc/group` do not exist on Windows, so `fopen` returns +//! `NULL` there naturally and this loop's existing `fail_label` path +//! handles it — no bespoke Windows behavior is needed. Byte-identical on +//! every other target. use crate::codegen_support::{abi, emit::Emitter, platform::Arch}; @@ -43,7 +52,7 @@ fn emit_lookup_x86_64(emitter: &mut Emitter, label: &str, path_symbol: &str) { emitter.instruction("mov QWORD PTR [rbp - 24], rsi"); // preserve principal byte length abi::emit_symbol_address(emitter, "rdi", path_symbol); abi::emit_symbol_address(emitter, "rsi", "_principal_lookup_read_mode"); - emitter.instruction("call fopen"); // open the local principal database for reading + emitter.emit_call_c("fopen"); // open the local principal database for reading emitter.instruction("test rax, rax"); // did fopen return a FILE pointer? emitter.instruction(&format!("je {fail_label}")); // missing database behaves like an unknown principal emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // preserve FILE pointer across loop calls @@ -52,13 +61,13 @@ fn emit_lookup_x86_64(emitter: &mut Emitter, label: &str, path_symbol: &str) { abi::emit_symbol_address(emitter, "rdi", "_principal_lookup_buf"); emitter.instruction("mov esi, 4096"); // maximum line bytes to read into the shared scratch buffer emitter.instruction("mov rdx, QWORD PTR [rbp - 8]"); // pass FILE pointer to fgets - emitter.instruction("call fgets"); // read one principal database line + emitter.emit_call_c("fgets"); // read one principal database line emitter.instruction("test rax, rax"); // did fgets return a line? emitter.instruction(&format!("je {close_fail_label}")); // EOF without a match returns not found abi::emit_symbol_address(emitter, "rdi", "_principal_lookup_buf"); emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // compare against the requested principal name emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // compare exactly the requested name length - emitter.instruction("call strncmp"); // compare database entry prefix with requested name + emitter.emit_call_c("strncmp"); // compare database entry prefix with requested name emitter.instruction("test eax, eax"); // did the prefix match? emitter.instruction(&format!("jne {loop_label}")); // keep scanning until the name matches abi::emit_symbol_address(emitter, "r8", "_principal_lookup_buf"); @@ -67,22 +76,22 @@ fn emit_lookup_x86_64(emitter: &mut Emitter, label: &str, path_symbol: &str) { emitter.instruction(&format!("jne {loop_label}")); // reject partial-prefix matches emitter.instruction("lea rdi, [r8 + rcx + 1]"); // search after the entry-name delimiter emitter.instruction("mov esi, 58"); // delimiter byte ':' - emitter.instruction("call strchr"); // locate the delimiter before uid/gid + emitter.emit_call_c("strchr"); // locate the delimiter before uid/gid emitter.instruction("test rax, rax"); // was the numeric field delimiter present? emitter.instruction(&format!("je {loop_label}")); // malformed line: keep scanning emitter.instruction("lea rdi, [rax + 1]"); // numeric id starts after the second delimiter emitter.instruction("xor esi, esi"); // endptr = NULL emitter.instruction("mov edx, 10"); // parse decimal uid/gid - emitter.instruction("call strtoul"); // parse the numeric principal id + emitter.emit_call_c("strtoul"); // parse the numeric principal id emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // preserve parsed id across fclose emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // close the database before returning a match - emitter.instruction("call fclose"); // release FILE handle + emitter.emit_call_c("fclose"); // release FILE handle emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // return parsed uid/gid emitter.instruction(&format!("jmp {done_label}")); // skip not-found sentinel emitter.label(&close_fail_label); emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // close the database after EOF - emitter.instruction("call fclose"); // release FILE handle + emitter.emit_call_c("fclose"); // release FILE handle emitter.label(&fail_label); emitter.instruction("mov rax, -1"); // not found sentinel diff --git a/src/codegen_support/runtime/io/proc_close.rs b/src/codegen_support/runtime/io/proc_close.rs new file mode 100644 index 0000000000..28de7306da --- /dev/null +++ b/src/codegen_support/runtime/io/proc_close.rs @@ -0,0 +1,229 @@ +//! Purpose: +//! Emits the `__rt_proc_close` runtime helper for every supported target. +//! Unix reaps by child PID; Windows waits on, queries, and closes its retained +//! process HANDLE. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via +//! `crate::codegen_support::runtime::io`, and from `__rt_mixed_free_deep` as +//! the kind-5 resource destructor arm. +//! +//! Key details: +//! - Unix reaps the child with `wait4(pid, &status, 0, 0)` and returns +//! `(status >> 8) & 0xff`; a normal exit previously harvested by +//! `proc_get_status` is returned from the status cache. Windows uses +//! `WaitForSingleObject` and `GetExitCodeProcess`. Failures return `-1` as the +//! PHP integer result. +//! - The lowerer (`lower_proc_close`) stamps a `-1` sentinel into the resource +//! box via `apply_resource_release_sentinel` so the kind-5 destructor arm in +//! `__rt_mixed_free_deep` later skips reaping. `proc_close` therefore owns the +//! reap and the destructor never re-reaps. +//! - Raw syscalls are emitted directly (not via `emitter.syscall()`/`map_syscall`) +//! so this helper stays self-contained and does not perturb the shared syscall +//! table. Linux `svc` does not set flags, so an explicit `cmp` precedes every +//! conditional branch on the AArch64 Linux path. + +use crate::codegen_support::{abi, emit::Emitter, platform::{Arch, Platform}}; + +/// Emits `__rt_proc_close`: reaps the child and returns its exit code. +/// +/// Input ABI: AArch64 `x0` = process descriptor (child pid); x86_64 `rdi` = pid. +/// Output: the child exit code (`0..255`) on success, or `-1` on `wait4` failure. +/// +/// Target dispatch: AArch64 (macOS + Linux) shares one emitter that branches on +/// `emitter.platform` for the syscall number/mechanism; Linux-x86_64 gets its own +/// System V AMD64 variant; Windows-x86_64 gets its own MSx64 +/// `WaitForSingleObject`/`GetExitCodeProcess` variant. +pub fn emit_proc_close(emitter: &mut Emitter) { + match emitter.target.arch { + Arch::AArch64 => emit_proc_close_aarch64(emitter), + Arch::X86_64 => { + if emitter.target.platform == Platform::Linux { + emit_proc_close_linux_x86_64(emitter); + } else { + emit_proc_close_win32_x86_64(emitter); + } + } + } +} + +/// Emits the AArch64 `__rt_proc_close` runtime (macOS and Linux). +/// +/// Uses a 32-byte frame: saved `x29`/`x30` at `[sp, #16]` and the wait-status +/// word at `[sp, #0]`. Branches on `emitter.platform` for the `wait4` syscall +/// number and trap convention (macOS `mov x16, #7` + `svc #0x80`; Linux +/// `mov x8, #260` + `svc #0`). Darwin reports syscall failure in carry, while +/// Linux returns a signed negative errno, so the target-specific tests must +/// run immediately after `svc` before any instruction overwrites NZCV. +fn emit_proc_close_aarch64(emitter: &mut Emitter) { + let is_macos = emitter.target.platform == Platform::MacOS; + emitter.blank(); + emitter.comment("--- runtime: proc_close (C1b reap) ---"); + emitter.label_global("__rt_proc_close"); + + // -- prologue: 32-byte frame for the saved link register and status word -- + emitter.instruction("sub sp, sp, #32"); // reserve the proc_close frame + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address + emitter.instruction("mov x29, sp"); // establish the helper frame pointer + + // -- close every associated pipe before waiting for the child -- + abi::emit_call_label(emitter, "__rt_proc_pipe_registry_close"); + abi::emit_call_label(emitter, "__rt_proc_status_cached_exit"); + emitter.instruction("str x1, [sp, #0]"); // retain cache flag across metadata teardown + emitter.instruction("str x2, [sp, #8]"); // retain cached normal exit code across teardown + abi::emit_call_label(emitter, "__rt_proc_status_unregister"); + emitter.instruction("ldr x9, [sp, #0]"); // did proc_get_status already reap a normal exit? + emitter.instruction("cbz x9, __rt_proc_close_wait"); // no cache still requires blocking wait4 + emitter.instruction("ldr x0, [sp, #8]"); // return the cached normal exit code + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the proc_close frame + emitter.instruction("ret"); // never wait twice for a cached child + emitter.label("__rt_proc_close_wait"); + + // -- wait4(pid, &status, 0, 0): x0 already holds the pid -- + emitter.instruction("add x1, sp, #0"); // status word address + emitter.instruction("mov x2, #0"); // no wait options + emitter.instruction("mov x3, #0"); // no rusage + if is_macos { + emitter.instruction("mov x16, #7"); // macOS wait4 syscall number + emitter.instruction("svc #0x80"); // macOS AArch64 trap + emitter.instruction("b.cs __rt_proc_close_err"); // Darwin reports wait4 failure through carry + } else { + emitter.instruction("mov x8, #260"); // Linux wait4 syscall number + emitter.instruction("svc #0"); // Linux AArch64 trap + emitter.instruction("cmp x0, #0"); // Linux wait4 returns a signed negative errno on failure + emitter.instruction("b.lt __rt_proc_close_err"); // bail out with -1 on a reap failure + } + + // -- extract the exit code: (status >> 8) & 0xff -- + emitter.instruction("ldr x9, [sp, #0]"); // reload the raw wait status + emitter.instruction("lsr x0, x9, #8"); // shift the exit code into the low byte + emitter.instruction("and x0, x0, #0xff"); // mask to the 0..255 exit-code range + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the frame + emitter.instruction("ret"); // return the child exit code + + emitter.label("__rt_proc_close_err"); + emitter.instruction("mov x0, #-1"); // report reap failure to the lowerer + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the frame + emitter.instruction("ret"); // return the failure sentinel +} + +/// Emits the Linux-x86_64 `__rt_proc_close` runtime (System V AMD64). +/// +/// Uses an `rbp` frame with the wait-status word at `[rbp-8]`. `rdi` already +/// holds the pid on entry. `wait4` is syscall 61 with `r10` carrying the +/// `rusage` argument (fourth syscall arg); the kernel clobbers `rcx`/`r11`. +fn emit_proc_close_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: proc_close (C1b reap) ---"); + emitter.label_global("__rt_proc_close"); + + // -- prologue: rbp frame with one status word -- + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish the helper frame pointer + emitter.instruction("sub rsp, 32"); // reserve cached-result and wait-status slots (16-byte aligned) + + // -- close every associated pipe before waiting for the child -- + abi::emit_call_label(emitter, "__rt_proc_pipe_registry_close"); + emitter.instruction("mov rdi, rax"); // restore pid after the registry helper returned it + abi::emit_call_label(emitter, "__rt_proc_status_cached_exit"); + emitter.instruction("mov QWORD PTR [rbp - 8], rdx"); // retain cache flag across metadata teardown + emitter.instruction("mov QWORD PTR [rbp - 16], rcx"); // retain cached normal exit code across metadata teardown + emitter.instruction("mov rdi, rax"); // restore pid before releasing its metadata + abi::emit_call_label(emitter, "__rt_proc_status_unregister"); + emitter.instruction("cmp QWORD PTR [rbp - 8], 0"); // did proc_get_status already reap a normal exit? + emitter.instruction("je __rt_proc_close_wait_x86"); // no cache still requires blocking wait4 + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // return the cached normal exit code + emitter.instruction("add rsp, 32"); // release cached-result slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // never wait twice for a cached child + emitter.label("__rt_proc_close_wait_x86"); + emitter.instruction("mov rdi, rax"); // restore pid after status metadata teardown + + // -- wait4(pid, &status, 0, 0): rdi already holds the pid -- + emitter.instruction("lea rsi, [rbp - 8]"); // status word address + emitter.instruction("xor edx, edx"); // no wait options + emitter.instruction("xor r10d, r10d"); // no rusage (fourth syscall arg via r10) + emitter.instruction("mov eax, 61"); // Linux x86_64 wait4 syscall number + emitter.instruction("syscall"); // Linux x86_64 trap (clobbers rcx/r11) + emitter.instruction("test rax, rax"); // wait4 retval < 0 means reap failure + emitter.instruction("js __rt_proc_close_err_x86"); // bail out with -1 on a reap failure + + // -- extract the exit code: (status >> 8) & 0xff -- + emitter.instruction("mov r9, QWORD PTR [rbp - 8]"); // reload the raw wait status + emitter.instruction("shr r9, 8"); // shift the exit code into the low byte + emitter.instruction("movzx eax, r9b"); // mask to the 0..255 exit-code range and return + emitter.instruction("add rsp, 32"); // release the frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the child exit code + + emitter.label("__rt_proc_close_err_x86"); + emitter.instruction("mov rax, -1"); // report reap failure to the lowerer + emitter.instruction("add rsp, 32"); // release the frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the failure sentinel +} + +/// Emits the Windows-x86_64 `__rt_proc_close` runtime (real C1c reap: MSx64 +/// `WaitForSingleObject` + `GetExitCodeProcess` + `CloseHandle`). +/// +/// Input: `rdi` = the process resource, an `hProcess` HANDLE (as returned by +/// `__rt_proc_open`'s `CreateProcessW`). Output: the child exit code +/// (`0..255`) on success, or `-1` on `WaitForSingleObject`/`GetExitCodeProcess` +/// failure. `rbp` frame: `[rbp - 8]` saves `hProcess` (`rcx`/`rdx` are +/// volatile and get clobbered between the three Win32 calls), `[rbp - 16]` +/// holds the DWORD exit code out-param for `GetExitCodeProcess`. 48 bytes +/// total (32-byte shadow space below `rsp` + the two 8-byte locals), 16-byte +/// aligned. +fn emit_proc_close_win32_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: proc_close (C1c reap) ---"); + emitter.label_global("__rt_proc_close"); + + // -- prologue: rbp frame, 48 bytes (shadow(32) + hProcess(8) + exit_code(8)) -- + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish the helper frame pointer + emitter.instruction("sub rsp, 48"); // reserve the proc_close frame + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save hProcess (rcx/rdx clobbered by calls) + + // -- close every associated pipe before waiting for the child -- + abi::emit_call_label(emitter, "__rt_proc_pipe_registry_close"); + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // refresh hProcess after the registry helper returned it + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // pass the preserved HANDLE to status-metadata teardown + abi::emit_call_label(emitter, "__rt_proc_status_unregister"); + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // release retained status metadata before consuming the HANDLE + + // -- WaitForSingleObject(hProcess, INFINITE): rdi already holds hProcess -- + emitter.instruction("mov rcx, QWORD PTR [rbp - 8]"); // hProcess + emitter.instruction("mov edx, 0xFFFFFFFF"); // dwMilliseconds = INFINITE + emitter.instruction("call WaitForSingleObject"); // block until the child process exits + emitter.instruction("cmp eax, 0xFFFFFFFF"); // WAIT_FAILED? + emitter.instruction("je __rt_proc_close_err_win"); // reap failure -> report -1 + + // -- GetExitCodeProcess(hProcess, &exit_code) -- + emitter.instruction("mov rcx, QWORD PTR [rbp - 8]"); // reload hProcess + emitter.instruction("lea rdx, [rbp - 16]"); // &exit_code out-param + emitter.instruction("call GetExitCodeProcess"); // fetch the child exit code + emitter.instruction("test eax, eax"); // GetExitCodeProcess failed? + emitter.instruction("jz __rt_proc_close_err_win"); // reap failure -> report -1 + + // -- CloseHandle(hProcess): the resource is fully consumed after this reap -- + emitter.instruction("mov rcx, QWORD PTR [rbp - 8]"); // reload hProcess + emitter.instruction("call CloseHandle"); // release the process handle + emitter.instruction("movsxd rax, DWORD PTR [rbp - 16]"); // reload the exit code as a SIGNED 32-bit value: Windows exit + // codes span the full DWORD range and php stores the DWORD into + // an int, so `cmd /c exit -1` must read back as -1, not 4294967295 + emitter.instruction("add rsp, 48"); // release the frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the child exit code + + emitter.label("__rt_proc_close_err_win"); + emitter.instruction("mov rcx, QWORD PTR [rbp - 8]"); // reload the consumed process HANDLE on every failure path + emitter.instruction("call CloseHandle"); // release the HANDLE even though reap status retrieval failed + emitter.instruction("mov rax, -1"); // report reap failure to the lowerer + emitter.instruction("add rsp, 48"); // release the frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the failure sentinel +} diff --git a/src/codegen_support/runtime/io/proc_open.rs b/src/codegen_support/runtime/io/proc_open.rs new file mode 100644 index 0000000000..66b81d745b --- /dev/null +++ b/src/codegen_support/runtime/io/proc_open.rs @@ -0,0 +1,2612 @@ +//! Purpose: +//! Emits the `__rt_proc_open` runtime helper, the C1b pipe-only `fork`/`pipe`/ +//! `execve` implementation for macOS-aarch64, Linux-aarch64, and Linux-x86_64. +//! Windows-x86_64 gets the real C1c implementation (`CreatePipe`/`CreateProcessW`) +//! including native `null`, `redirect`, stream-resource, and file descriptors. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via +//! `crate::codegen_support::runtime::io`. +//! +//! Key details: +//! - Pipe-only: descriptors must be `["pipe", mode]`. Pipe direction follows +//! php-src: a string mode whose first byte is `w` makes the child write; every +//! other string mode (including `r`, `rb`, and the empty string) makes the +//! child read. PHP weakly coerces scalar non-string modes to strings; their +//! decimal/boolean/null spellings cannot start with `w`, so they also make +//! the child read without an unnecessary temporary string allocation. +//! Any other descriptor shape makes +//! `proc_open` return `-1` (documented C1b limitation; `["file",...]` and +//! friends are a follow-up). The descriptor count is bounded at 8. The +//! descriptor_spec array may be either indexed (kind 2) or hash (kind 3); +//! `__rt_array_get_mixed_key` is representation-agnostic and handles both. +//! - ABI (set by the EIR lowerer, never changed here): AArch64 `x0` = +//! descriptor_spec, `x1`/`x2` = command ptr/len, `x3` = pipes, `x4`/`x5` = cwd, +//! `x6` = environment block and `x7` = packed length/direct flags. x86_64 uses +//! `rdi` through `r9` plus two stack arguments. Returns the raw child pid (`>= 0`) on +//! success or `-1` on failure; the lowerer boxes `>= 0` as `Mixed(resource, +//! kind=5)` and `< 0` as `Mixed(false)`. +//! - `$pipes` may be promoted/reallocated on every target when a sparse +//! descriptor key is written. The final container pointer is consequently +//! returned in the paired secondary result register (`x1`/`rdx`) and the EIR +//! backend writes it through the original by-reference local. Pipe resources +//! retain their real integer descriptor keys rather than being appended or +//! reindexed. +//! - Raw syscalls are emitted directly (not via `emitter.syscall()`/`map_syscall`) +//! so this helper stays self-contained. Linux `svc` does not set flags, so an +//! explicit `cmp x0, #0` precedes every conditional branch on AArch64 Linux. +//! - macOS fork caveat: the raw `fork` syscall (2) returns the child pid in BOTH +//! the parent and the child process (not 0 in the child). Parent/child are +//! distinguished by comparing `getpid()` before and after fork — a different +//! pid means the child. Linux's `clone` returns 0 in the child as expected. +//! - macOS close caveat: `SYS_close` is syscall 6 on macOS (NOT 3, which is +//! `SYS_read`). Using 3 would call `read()` on the pipe fd and block forever. + +use crate::codegen_support::{abi, emit::Emitter, platform::{Arch, Platform}}; + +/// Emits `__rt_proc_open`: pipe-only `proc_open` returning the child pid. +/// +/// Input ABI: AArch64 `x0` = descriptor_spec, `x1`/`x2` = command ptr/len, +/// `x3` = pipes array ptr; x86_64 `rdi`/`rsi`/`rdx`/`rcx`. Output: the raw child +/// pid (`>= 0`) on success, or `-1` on failure (the lowerer does the boxing). +/// Every target returns the final `$pipes` container in the paired secondary +/// result register. +/// +/// Target dispatch: AArch64 (macOS + Linux) shares one emitter that branches on +/// `emitter.platform` for syscall numbers/mechanisms; Linux-x86_64 gets its own +/// System V AMD64 variant; Windows-x86_64 gets its own MSx64 `CreatePipe`/ +/// `CreateProcessW` variant. +pub fn emit_proc_open(emitter: &mut Emitter) { + match emitter.target.arch { + Arch::AArch64 => emit_proc_open_aarch64(emitter), + Arch::X86_64 => { + if emitter.target.platform == Platform::Linux { + emit_proc_open_linux_x86_64(emitter); + } else { + emit_proc_open_win32_x86_64(emitter); + emit_proc_open_socketpair_win(emitter); + } + } + } +} + +/// Emits the AArch64 `__rt_proc_open` runtime (macOS and Linux). +/// +/// 496-byte frame layout (offsets from `sp`): +/// `[0]` x29, `[8]` x30, `[16]` desc, `[24]` cmd_ptr, `[32]` cmd_len, +/// `[40]` pipes, `[48]` n, `[56]` pipe_count, `[64]` i, `[72]` sub_box, +/// `[80]` sub_ptr (reused for `is_read` after the mode read), `[88]` m0, +/// `[96]` m1, `[104]`/`[112]` pipe_fds, `[120..184)` parent_fd, +/// `[184..248)` child_fd, `[248..312)` is_pipe, `[312]` path_buf, +/// `[320]` argv0, `[328]` argv1, `[336]` cmd_cstr, `[344..376)` argv, +/// `[376]` lit_pipe, `[384]` lit_r, `[392]` pid, `[400]` cleanup_j, +/// `[408]` parent_pid (macOS fork parent/child disambiguation), +/// `[416..480)` descriptor_index[8], `[480]` hash cursor, `[488]` spec kind. +fn emit_proc_open_aarch64(emitter: &mut Emitter) { + let is_macos = emitter.target.platform == Platform::MacOS; + emitter.blank(); + emitter.comment("--- runtime: proc_open (C1b pipe-only) ---"); + emitter.label_global("__rt_proc_open"); + + // -- prologue: 496-byte bookkeeping frame -- + emitter.instruction("sub sp, sp, #496"); // reserve the proc_open frame + emitter.instruction("stp x29, x30, [sp, #0]"); // save frame pointer and return address + emitter.instruction("mov x29, sp"); // establish the helper frame pointer + emitter.instruction("str x0, [sp, #16]"); // save the descriptor_spec array pointer + emitter.instruction("str x1, [sp, #24]"); // save the command string pointer + emitter.instruction("str x2, [sp, #32]"); // save the command string length + emitter.instruction("str x3, [sp, #40]"); // save the pipes array pointer + + // -- validate descriptor_spec: non-null + indexed kind + 1 <= n <= 8 -- + emitter.instruction("cbz x0, __rt_proc_open_fail"); // a null descriptor_spec is unrecoverable + emitter.instruction("ldr x9, [x0, #-8]"); // load the packed array kind metadata + emitter.instruction("and x9, x9, #0xff"); // isolate the low-byte storage kind + emitter.instruction("cmp x9, #2"); // kind 2 = indexed array? + emitter.instruction("b.eq __rt_proc_open_kind_ok"); // accept indexed-array descriptor_spec + emitter.instruction("cmp x9, #3"); // kind 3 = hash storage? + emitter.instruction("b.ne __rt_proc_open_fail"); // non-array descriptor_spec is unsupported + emitter.label("__rt_proc_open_kind_ok"); + emitter.instruction("ldr x9, [x0]"); // read the descriptor_spec length + emitter.instruction("cmp x9, #0"); // an empty descriptor spec is invalid + emitter.instruction("b.eq __rt_proc_open_fail"); // bail out on an empty descriptor spec + emitter.instruction("cmp x9, #8"); // C1b bounds the descriptor count at 8 + emitter.instruction("b.gt __rt_proc_open_fail"); // refuse an over-long descriptor spec + emitter.instruction("str x9, [sp, #48]"); // save the descriptor count n + emitter.instruction("ldr x9, [x0, #-8]"); // reload descriptor storage metadata after the count read + emitter.instruction("and x9, x9, #0xff"); // isolate the indexed/hash kind for position iteration + emitter.instruction("str x9, [sp, #488]"); // retain descriptor storage kind across runtime helper calls + emitter.instruction("str xzr, [sp, #480]"); // start the hash insertion-order cursor at its head + + // -- zero the is_pipe bookkeeping array so cleanup is safe before any pipe opens -- + emitter.instruction("str xzr, [sp, #248]"); // is_pipe[0] = 0 + emitter.instruction("str xzr, [sp, #256]"); // is_pipe[1] = 0 + emitter.instruction("str xzr, [sp, #264]"); // is_pipe[2] = 0 + emitter.instruction("str xzr, [sp, #272]"); // is_pipe[3] = 0 + emitter.instruction("str xzr, [sp, #280]"); // is_pipe[4] = 0 + emitter.instruction("str xzr, [sp, #288]"); // is_pipe[5] = 0 + emitter.instruction("str xzr, [sp, #296]"); // is_pipe[6] = 0 + emitter.instruction("str xzr, [sp, #304]"); // is_pipe[7] = 0 + + // -- main descriptor loop: for i = 0 .. n-1, open one pipe per descriptor -- + emitter.instruction("str xzr, [sp, #64]"); // i = 0 + emitter.label("__rt_proc_open_loop_test"); + emitter.instruction("ldr x9, [sp, #64]"); // reload the loop index + emitter.instruction("ldr x10, [sp, #48]"); // reload n + emitter.instruction("cmp x9, x10"); // i < n? + emitter.instruction("b.ge __rt_proc_open_fork"); // descriptor loop complete -> fork + + // -- resolve position i to its real integer descriptor key, then read it -- + emitter.instruction("ldr x9, [sp, #488]"); // reload descriptor storage kind + emitter.instruction("cmp x9, #3"); // descriptor spec is associative hash storage? + emitter.instruction("b.ne __rt_proc_open_indexed_key"); // indexed positions are their integer keys + emitter.instruction("ldr x0, [sp, #16]"); // hash descriptor-spec pointer + emitter.instruction("ldr x1, [sp, #480]"); // insertion-order hash cursor + abi::emit_call_label(emitter, "__rt_hash_iter_next"); + emitter.instruction("str x0, [sp, #480]"); // preserve the returned hash cursor + emitter.instruction("cmn x2, #1"); // only integer descriptor keys are valid + emitter.instruction("b.ne __rt_proc_open_fail"); // string keys cannot name child descriptors + emitter.instruction("str x1, [sp, #400]"); // retain the actual descriptor integer key for this loop position + emitter.instruction("b __rt_proc_open_descriptor_key_ready"); // skip indexed key synthesis + emitter.label("__rt_proc_open_indexed_key"); + emitter.instruction("ldr x9, [sp, #64]"); // indexed position is the descriptor integer key + emitter.instruction("str x9, [sp, #400]"); // retain the descriptor integer key for this loop position + emitter.label("__rt_proc_open_descriptor_key_ready"); + emitter.instruction("ldr x0, [sp, #16]"); // descriptor_spec array pointer + emitter.instruction("ldr x1, [sp, #400]"); // key = actual descriptor integer key + emitter.instruction("mov x2, #-1"); // int-key sentinel + emitter.instruction("mov x3, #0"); // suppress missing-key warnings + abi::emit_call_label(emitter, "__rt_array_get_mixed_key"); + emitter.instruction("str x0, [sp, #72]"); // save the owned sub_box across unbox + + // -- unbox sub_box: expect an indexed array (runtime tag 4) -- + abi::emit_call_label(emitter, "__rt_mixed_unbox"); // x0=tag, x1=lo (sub ptr), x2=hi + emitter.instruction("cmp x0, #4"); // tag 4 = indexed array? + emitter.instruction("b.ne __rt_proc_open_cleanup_sub"); // non-array descriptor -> cleanup + fail + emitter.instruction("str x1, [sp, #80]"); // save the sub-array pointer for element reads + + // -- read sub[0] (the descriptor type string) as an owned box -- + emitter.instruction("ldr x0, [sp, #80]"); // sub-array pointer + emitter.instruction("mov x1, #0"); // key = 0 + emitter.instruction("mov x2, #-1"); // int-key sentinel + emitter.instruction("mov x3, #0"); // suppress missing-key warnings + abi::emit_call_label(emitter, "__rt_array_get_mixed_key"); + emitter.instruction("str x0, [sp, #88]"); // save m0 (descriptor type string box) + + // -- unbox m0: expect a string (runtime tag 1) -- + abi::emit_call_label(emitter, "__rt_mixed_unbox"); // x0=tag, x1=ptr, x2=len + emitter.instruction("cmp x0, #1"); // tag 1 = string? + emitter.instruction("b.ne __rt_proc_open_cleanup_m0"); // non-string descriptor type -> cleanup + fail + + // -- build the literal "pipe" on the stack and compare against sub[0] -- + emitter.instruction("mov w9, #0x70"); // 'p' + emitter.instruction("strb w9, [sp, #376]"); // lit_pipe[0] = 'p' + emitter.instruction("mov w9, #0x69"); // 'i' + emitter.instruction("strb w9, [sp, #377]"); // lit_pipe[1] = 'i' + emitter.instruction("mov w9, #0x70"); // 'p' + emitter.instruction("strb w9, [sp, #378]"); // lit_pipe[2] = 'p' + emitter.instruction("mov w9, #0x65"); // 'e' + emitter.instruction("strb w9, [sp, #379]"); // lit_pipe[3] = 'e' + // __rt_str_eq(ptr_a, len_a, ptr_b, len_b): x1/x2 already hold ptr/len from unbox + emitter.instruction("add x3, sp, #376"); // ptr_b = &lit_pipe + emitter.instruction("mov x4, #4"); // len_b = 4 + abi::emit_call_label(emitter, "__rt_str_eq"); // x0 = 1 if "pipe" else 0 + emitter.instruction("cbz x0, __rt_proc_open_cleanup_m0"); // non-pipe descriptor unsupported in C1b -> cleanup + fail + + // -- read sub[1] (the mode string) as an owned box -- + emitter.instruction("ldr x0, [sp, #80]"); // sub-array pointer + emitter.instruction("mov x1, #1"); // key = 1 + emitter.instruction("mov x2, #-1"); // int-key sentinel + emitter.instruction("mov x3, #0"); // suppress missing-key warnings + abi::emit_call_label(emitter, "__rt_array_get_mixed_key"); + emitter.instruction("str x0, [sp, #96]"); // save m1 (mode string box) + + // -- unbox m1: strings select their first byte; PHP scalar coercions read -- + abi::emit_call_label(emitter, "__rt_mixed_unbox"); // x0=tag, x1=ptr, x2=len + emitter.instruction("cmp x0, #1"); // tag 1 = string? + emitter.instruction("b.eq __rt_proc_open_string_mode"); // strings retain php-src's first-byte direction rule + emitter.instruction("cmp x0, #0"); // tag 0 = integer scalar? + emitter.instruction("b.eq __rt_proc_open_scalar_mode_read"); // decimal integer text cannot begin with `w` + emitter.instruction("cmp x0, #2"); // tag 2 = float scalar? + emitter.instruction("b.eq __rt_proc_open_scalar_mode_read"); // float text cannot begin with `w` + emitter.instruction("cmp x0, #3"); // tag 3 = boolean scalar? + emitter.instruction("b.eq __rt_proc_open_scalar_mode_read"); // boolean text cannot begin with `w` + emitter.instruction("cmp x0, #8"); // tag 8 = null scalar? + emitter.instruction("b.ne __rt_proc_open_cleanup_m1"); // non-scalar mode remains unsupported by this runtime + emitter.label("__rt_proc_open_scalar_mode_read"); + emitter.instruction("mov x9, #1"); // PHP scalar-to-string coercion produces a non-write-leading mode + emitter.instruction("b __rt_proc_open_mode_direction_ready"); // release owned boxes using the common direction path + + // -- match php-src: only a leading "w" changes the pipe direction -- + emitter.label("__rt_proc_open_string_mode"); + emitter.instruction("cbz x2, __rt_proc_open_empty_mode_read"); // an empty mode follows php-src's non-write/read direction + emitter.instruction("ldrb w9, [x1]"); // load the mode's first byte + emitter.instruction("cmp w9, #0x77"); // first byte is 'w'? + emitter.instruction("cset x9, ne"); // is_read = first byte is not 'w' + emitter.instruction("b __rt_proc_open_mode_direction_ready"); // keep the computed non-empty mode direction + emitter.label("__rt_proc_open_empty_mode_read"); + emitter.instruction("mov x9, #1"); // empty modes also make the child read + emitter.label("__rt_proc_open_mode_direction_ready"); + emitter.instruction("str x9, [sp, #80]"); // save is_read in the sub_ptr slot (no longer needed) + + // -- release the three owned boxes now that is_read is known -- + emitter.instruction("ldr x0, [sp, #88]"); // m0 + abi::emit_call_label(emitter, "__rt_decref_mixed"); // drop the caller's ref on m0 + emitter.instruction("ldr x0, [sp, #96]"); // m1 + abi::emit_call_label(emitter, "__rt_decref_mixed"); // drop the caller's ref on m1 + emitter.instruction("ldr x0, [sp, #72]"); // sub_box + abi::emit_call_label(emitter, "__rt_decref_mixed"); // drop the caller's ref on sub_box + + // -- open a pipe pair: macOS pipe (fds in x0/x1), Linux pipe2 (fds in buffer) -- + if is_macos { + // macOS pipe syscall returns read_fd in x0, write_fd in x1 (no buffer arg). + emitter.instruction("mov x16, #42"); // macOS pipe syscall number + emitter.instruction("svc #0x80"); // x0 = read_fd, x1 = write_fd + emitter.instruction("cmp x0, #0"); // x0 < 0 means error (-errno) + emitter.instruction("b.lt __rt_proc_open_cleanup"); // pipe open failed -> cleanup opened pipes + fail + emitter.instruction("str w0, [sp, #104]"); // save read_end as 32-bit int + emitter.instruction("str w1, [sp, #108]"); // save write_end as 32-bit int + } else { + // Linux pipe2 writes fds to the buffer and returns 0/-1. + emitter.instruction("add x0, sp, #104"); // &pipe_fds[0] + emitter.instruction("mov x1, #0"); // pipe2 flags = 0 + emitter.instruction("mov x8, #59"); // Linux pipe2 syscall number + emitter.instruction("svc #0"); // Linux AArch64 trap + emitter.instruction("cmp x0, #0"); // pipe2 retval < 0 means failure + emitter.instruction("b.lt __rt_proc_open_cleanup"); // pipe open failed -> cleanup opened pipes + fail + } + + // -- record the read/write ends per mode: child gets the mode end, parent the other -- + emitter.instruction("ldr w10, [sp, #104]"); // read_end = pipe_fds[0] (32-bit int) + emitter.instruction("ldr w11, [sp, #108]"); // write_end = pipe_fds[1] (32-bit int) + emitter.instruction("ldr x12, [sp, #64]"); // i (descriptor index) + emitter.instruction("add x13, sp, #184"); // base of child_fd + emitter.instruction("add x13, x13, x12, lsl #3"); // &child_fd[i] + emitter.instruction("add x14, sp, #120"); // base of parent_fd + emitter.instruction("add x14, x14, x12, lsl #3"); // &parent_fd[i] + emitter.instruction("ldr x9, [sp, #80]"); // reload is_read + emitter.instruction("cbz x9, __rt_proc_open_write_mode"); // a write-leading mode gives the child write_end + emitter.instruction("str x10, [x13]"); // non-write-leading mode gives the child read_end + emitter.instruction("str x11, [x14]"); // non-write-leading mode gives the parent write_end + emitter.instruction("b __rt_proc_open_pipe_recorded"); // skip the write-mode assignment + emitter.label("__rt_proc_open_write_mode"); + emitter.instruction("str x11, [x13]"); // write-leading mode gives the child write_end + emitter.instruction("str x10, [x14]"); // write-leading mode gives the parent read_end + emitter.label("__rt_proc_open_pipe_recorded"); + emitter.instruction("mov x9, #1"); // is_pipe sentinel + emitter.instruction("add x13, sp, #248"); // base of is_pipe + emitter.instruction("add x13, x13, x12, lsl #3"); // &is_pipe[i] + emitter.instruction("str x9, [x13]"); // is_pipe[i] = 1 + emitter.instruction("add x13, sp, #416"); // base of descriptor_index bookkeeping + emitter.instruction("ldr x9, [sp, #64]"); // reload loop position i + emitter.instruction("ldr x10, [sp, #400]"); // reload the real descriptor integer key + emitter.instruction("str x10, [x13, x9, lsl #3]"); // descriptor_index[i] = real key for child/output phases + + // -- advance the loop index -- + emitter.instruction("ldr x9, [sp, #64]"); // reload i + emitter.instruction("add x9, x9, #1"); // i += 1 + emitter.instruction("str x9, [sp, #64]"); // persist the loop index + emitter.instruction("b __rt_proc_open_loop_test"); // continue the descriptor loop + + // -- fork/clone: macOS fork=2, Linux clone(SIGCHLD) = 220 -- + // macOS caveat: the raw fork syscall returns the child pid in BOTH the + // parent and the child (not 0 in the child). We distinguish parent from + // child by comparing getpid() before and after fork. Linux's clone returns + // 0 in the child as expected. + emitter.label("__rt_proc_open_fork"); + if is_macos { + emitter.instruction("mov x16, #20"); // getpid before fork + emitter.instruction("svc #0x80"); // x0 = parent pid + emitter.instruction("str x0, [sp, #408]"); // save parent pid for disambiguation + emitter.instruction("mov x16, #2"); // macOS fork syscall number + emitter.instruction("svc #0x80"); // x0 = child pid (both processes) + emitter.instruction("str x0, [sp, #392]"); // save fork retval (pid or -1) + emitter.instruction("cmp x0, #0"); // fork retval < 0 means failure + emitter.instruction("b.lt __rt_proc_open_cleanup"); // fork failed -> close all opened pipes + fail + emitter.instruction("mov x16, #20"); // getpid after fork + emitter.instruction("svc #0x80"); // x0 = current pid + emitter.instruction("ldr x10, [sp, #408]"); // reload the saved parent pid + emitter.instruction("cmp x0, x10"); // current pid == parent pid? + emitter.instruction("b.ne __rt_proc_open_child"); // different pid -> child branch + // parent: child pid already saved at [sp, #392] + } else { + emitter.instruction("mov x0, #17"); // clone flags = SIGCHLD + emitter.instruction("mov x1, #0"); // child_stack = 0 (fork semantics) + emitter.instruction("mov x2, #0"); // ptid = 0 + emitter.instruction("mov x3, #0"); // ctid = 0 + emitter.instruction("mov x4, #0"); // tls = 0 + emitter.instruction("mov x8, #220"); // Linux clone syscall number + emitter.instruction("svc #0"); // Linux AArch64 trap + emitter.instruction("cmp x0, #0"); // fork retval < 0 means failure + emitter.instruction("b.lt __rt_proc_open_cleanup"); // fork failed -> close all opened pipes + fail + emitter.instruction("b.eq __rt_proc_open_child"); // pid == 0 -> child branch + emitter.instruction("str x0, [sp, #392]"); // parent: save the child pid + } + + // -- parent: close every child end, then push the parent ends into $pipes -- + emitter.instruction("str xzr, [sp, #64]"); // j = 0 + emitter.label("__rt_proc_open_parent_close_test"); + emitter.instruction("ldr x9, [sp, #64]"); // reload j + emitter.instruction("ldr x10, [sp, #48]"); // reload n + emitter.instruction("cmp x9, x10"); // j < n? + emitter.instruction("b.ge __rt_proc_open_parent_push"); // all child ends closed -> push phase + emitter.instruction("add x11, sp, #248"); // base of is_pipe + emitter.instruction("ldr x12, [x11, x9, lsl #3]"); // is_pipe[j] + emitter.instruction("cbz x12, __rt_proc_open_parent_close_next"); // not a pipe -> skip + emitter.instruction("add x11, sp, #184"); // base of child_fd + emitter.instruction("ldr x0, [x11, x9, lsl #3]"); // child_fd[j] + if is_macos { + emitter.instruction("mov x16, #6"); // macOS close syscall number (SYS_close=6) + emitter.instruction("svc #0x80"); // macOS AArch64 trap + } else { + emitter.instruction("mov x8, #57"); // Linux close syscall number + emitter.instruction("svc #0"); // Linux AArch64 trap + } + emitter.label("__rt_proc_open_parent_close_next"); + emitter.instruction("ldr x9, [sp, #64]"); // reload j + emitter.instruction("add x9, x9, #1"); // j += 1 + emitter.instruction("str x9, [sp, #64]"); // persist j + emitter.instruction("b __rt_proc_open_parent_close_test"); // continue closing child ends + + // -- parent publish phase: write each parent end at its descriptor key -- + emitter.label("__rt_proc_open_parent_push"); + emitter.instruction("str xzr, [sp, #64]"); // j = 0 + emitter.instruction("str xzr, [sp, #56]"); // pipe_count = 0 + emitter.label("__rt_proc_open_parent_push_test"); + emitter.instruction("ldr x9, [sp, #64]"); // reload j + emitter.instruction("ldr x10, [sp, #48]"); // reload n + emitter.instruction("cmp x9, x10"); // j < n? + emitter.instruction("b.ge __rt_proc_open_done"); // all pipes pushed -> return pid + emitter.instruction("add x11, sp, #248"); // base of is_pipe + emitter.instruction("ldr x12, [x11, x9, lsl #3]"); // is_pipe[j] + emitter.instruction("cbz x12, __rt_proc_open_parent_push_next"); // not a pipe -> skip + emitter.instruction("add x11, sp, #120"); // base of parent_fd + emitter.instruction("ldr x1, [x11, x9, lsl #3]"); // parent_fd[j] (resource handle lo) + emitter.instruction("mov x0, #9"); // tag 9 = resource + emitter.instruction("mov x2, #1"); // hi = kind 1 (native stream fd) + abi::emit_call_label(emitter, "__rt_mixed_from_value"); // x0 = res_box (owned, refcount 1) + emitter.instruction("str x0, [sp, #72]"); // save the owned resource box for the keyed container write + emitter.instruction("ldr x9, [sp, #64]"); // reload j after the allocating Mixed helper clobbered caller-saved registers + emitter.instruction("add x11, sp, #416"); // base of descriptor_index bookkeeping + emitter.instruction("ldr x1, [x11, x9, lsl #3]"); // real integer descriptor key for this parent pipe + emitter.instruction("mov x0, #0"); // tag 0 = integer key Mixed cell + emitter.instruction("mov x2, #0"); // integer key has no high payload word + abi::emit_call_label(emitter, "__rt_mixed_from_value"); // allocate the borrowed key cell used by the generic setter + emitter.instruction("str x0, [sp, #88]"); // retain the key cell across the container write + emitter.instruction("ldr x0, [sp, #40]"); // reload the current pipes array/hash pointer + emitter.instruction("ldr x1, [sp, #88]"); // pass the integer descriptor key cell + emitter.instruction("ldr x2, [sp, #72]"); // transfer the resource box into the destination container + abi::emit_call_label(emitter, "__rt_array_set_mixed_key"); // set resource at the real descriptor key, returning promoted/reallocated container + emitter.instruction("str x0, [sp, #40]"); // persist the potentially promoted/reallocated pipes container + emitter.instruction("ldr x0, [sp, #88]"); // reload the borrowed key cell after the setter read it + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the caller-owned key box; the setter never owns keys + emitter.label("__rt_proc_open_parent_push_next"); + emitter.instruction("ldr x9, [sp, #64]"); // reload j + emitter.instruction("add x9, x9, #1"); // j += 1 + emitter.instruction("str x9, [sp, #64]"); // persist j + emitter.instruction("b __rt_proc_open_parent_push_test"); // continue pushing parent ends + + // -- child: dup2 each child end onto descriptor fd i, close the parent ends, execve -- + emitter.label("__rt_proc_open_child"); + emitter.instruction("str xzr, [sp, #64]"); // j = 0 + emitter.label("__rt_proc_open_child_dup_test"); + emitter.instruction("ldr x9, [sp, #64]"); // reload j + emitter.instruction("ldr x10, [sp, #48]"); // reload n + emitter.instruction("cmp x9, x10"); // j < n? + emitter.instruction("b.ge __rt_proc_open_child_close_phase"); // all dups done -> close parent ends + emitter.instruction("add x11, sp, #248"); // base of is_pipe + emitter.instruction("ldr x12, [x11, x9, lsl #3]"); // is_pipe[j] + emitter.instruction("cbz x12, __rt_proc_open_child_dup_next"); // not a pipe -> skip + emitter.instruction("add x11, sp, #184"); // base of child_fd + emitter.instruction("ldr x0, [x11, x9, lsl #3]"); // child_fd[j] (old fd) + emitter.instruction("add x11, sp, #416"); // base of descriptor_index bookkeeping + emitter.instruction("ldr x1, [x11, x9, lsl #3]"); // new fd = real descriptor integer key + if is_macos { + emitter.instruction("mov x16, #90"); // macOS dup2 syscall number + emitter.instruction("svc #0x80"); // macOS AArch64 trap + } else { + emitter.instruction("mov x2, #0"); // dup3 flags = 0 + emitter.instruction("mov x8, #24"); // Linux dup3 syscall number + emitter.instruction("svc #0"); // Linux AArch64 trap + } + // -- if child_fd[j] != j, close the now-redundant original child end -- + emitter.instruction("ldr x9, [sp, #64]"); // reload j (preserved across svc in x9) + emitter.instruction("add x11, sp, #184"); // base of child_fd + emitter.instruction("ldr x13, [x11, x9, lsl #3]"); // reload child_fd[j] + emitter.instruction("add x11, sp, #416"); // base of descriptor_index bookkeeping + emitter.instruction("ldr x10, [x11, x9, lsl #3]"); // reload the real descriptor integer key + emitter.instruction("cmp x13, x10"); // child_fd[j] == descriptor key? + emitter.instruction("b.eq __rt_proc_open_child_dup_next"); // equal -> no close needed (fd is already the dup target) + emitter.instruction("mov x0, x13"); // close(child_fd[j]) + if is_macos { + emitter.instruction("mov x16, #6"); // macOS close syscall number (SYS_close=6) + emitter.instruction("svc #0x80"); // macOS AArch64 trap + } else { + emitter.instruction("mov x8, #57"); // Linux close syscall number + emitter.instruction("svc #0"); // Linux AArch64 trap + } + emitter.label("__rt_proc_open_child_dup_next"); + emitter.instruction("ldr x9, [sp, #64]"); // reload j + emitter.instruction("add x9, x9, #1"); // j += 1 + emitter.instruction("str x9, [sp, #64]"); // persist j + emitter.instruction("b __rt_proc_open_child_dup_test"); // continue the dup loop + + // -- child: close every parent end so the child does not hold the parent's pipe side -- + emitter.label("__rt_proc_open_child_close_phase"); + emitter.instruction("str xzr, [sp, #64]"); // j = 0 + emitter.label("__rt_proc_open_child_close_test"); + emitter.instruction("ldr x9, [sp, #64]"); // reload j + emitter.instruction("ldr x10, [sp, #48]"); // reload n + emitter.instruction("cmp x9, x10"); // j < n? + emitter.instruction("b.ge __rt_proc_open_child_exec"); // all parent ends closed -> execve + emitter.instruction("add x11, sp, #248"); // base of is_pipe + emitter.instruction("ldr x12, [x11, x9, lsl #3]"); // is_pipe[j] + emitter.instruction("cbz x12, __rt_proc_open_child_close_next"); // not a pipe -> skip + emitter.instruction("add x11, sp, #120"); // base of parent_fd + emitter.instruction("ldr x0, [x11, x9, lsl #3]"); // parent_fd[j] + if is_macos { + emitter.instruction("mov x16, #6"); // macOS close syscall number (SYS_close=6) + emitter.instruction("svc #0x80"); // macOS AArch64 trap + } else { + emitter.instruction("mov x8, #57"); // Linux close syscall number + emitter.instruction("svc #0"); // Linux AArch64 trap + } + emitter.label("__rt_proc_open_child_close_next"); + emitter.instruction("ldr x9, [sp, #64]"); // reload j + emitter.instruction("add x9, x9, #1"); // j += 1 + emitter.instruction("str x9, [sp, #64]"); // persist j + emitter.instruction("b __rt_proc_open_child_close_test"); // continue closing parent ends + + // -- child: build the execve payload and exec /bin/sh -c -- + emitter.label("__rt_proc_open_child_exec"); + emitter.instruction("ldr x1, [sp, #24]"); // command pointer into __rt_cstr input + emitter.instruction("ldr x2, [sp, #32]"); // command length into __rt_cstr input + abi::emit_call_label(emitter, "__rt_cstr"); // x0 = null-terminated command string + emitter.instruction("str x0, [sp, #336]"); // save cmd_cstr for argv[2] + // -- store "/bin/sh\0" into path_buf -- + emitter.instruction("mov w9, #0x2f"); // '/' + emitter.instruction("strb w9, [sp, #312]"); // path_buf[0] = '/' + emitter.instruction("mov w9, #0x62"); // 'b' + emitter.instruction("strb w9, [sp, #313]"); // path_buf[1] = 'b' + emitter.instruction("mov w9, #0x69"); // 'i' + emitter.instruction("strb w9, [sp, #314]"); // path_buf[2] = 'i' + emitter.instruction("mov w9, #0x6e"); // 'n' + emitter.instruction("strb w9, [sp, #315]"); // path_buf[3] = 'n' + emitter.instruction("mov w9, #0x2f"); // '/' + emitter.instruction("strb w9, [sp, #316]"); // path_buf[4] = '/' + emitter.instruction("mov w9, #0x73"); // 's' + emitter.instruction("strb w9, [sp, #317]"); // path_buf[5] = 's' + emitter.instruction("mov w9, #0x68"); // 'h' + emitter.instruction("strb w9, [sp, #318]"); // path_buf[6] = 'h' + emitter.instruction("strb wzr, [sp, #319]"); // path_buf[7] = NUL + // -- store "sh\0" into argv0 -- + emitter.instruction("mov w9, #0x73"); // 's' + emitter.instruction("strb w9, [sp, #320]"); // argv0[0] = 's' + emitter.instruction("mov w9, #0x68"); // 'h' + emitter.instruction("strb w9, [sp, #321]"); // argv0[1] = 'h' + emitter.instruction("strb wzr, [sp, #322]"); // argv0[2] = NUL + // -- store "-c\0" into argv1 -- + emitter.instruction("mov w9, #0x2d"); // '-' + emitter.instruction("strb w9, [sp, #328]"); // argv1[0] = '-' + emitter.instruction("mov w9, #0x63"); // 'c' + emitter.instruction("strb w9, [sp, #329]"); // argv1[1] = 'c' + emitter.instruction("strb wzr, [sp, #330]"); // argv1[2] = NUL + // -- build argv[4] = { &argv0, &argv1, cmd_cstr, NULL } -- + emitter.instruction("add x9, sp, #320"); // &argv0 + emitter.instruction("str x9, [sp, #344]"); // argv[0] = &argv0 + emitter.instruction("add x9, sp, #328"); // &argv1 + emitter.instruction("str x9, [sp, #352]"); // argv[1] = &argv1 + emitter.instruction("ldr x9, [sp, #336]"); // cmd_cstr + emitter.instruction("str x9, [sp, #360]"); // argv[2] = cmd_cstr + emitter.instruction("str xzr, [sp, #368]"); // argv[3] = NULL + // -- execve(path_buf, argv, NULL) -- + emitter.instruction("add x0, sp, #312"); // path = &path_buf + emitter.instruction("add x1, sp, #344"); // argv = &argv[0] + emitter.instruction("mov x2, #0"); // envp = NULL + if is_macos { + emitter.instruction("mov x16, #59"); // macOS execve syscall number + emitter.instruction("svc #0x80"); // macOS AArch64 trap + } else { + emitter.instruction("mov x8, #221"); // Linux execve syscall number + emitter.instruction("svc #0"); // Linux AArch64 trap + } + // -- execve returned (failure): exit the child with status 127 -- + emitter.instruction("mov x0, #127"); // child exit status for execve failure + if is_macos { + emitter.instruction("mov x16, #1"); // macOS exit syscall number + emitter.instruction("svc #0x80"); // macOS AArch64 trap (does not return) + } else { + emitter.instruction("mov x8, #93"); // Linux exit syscall number + emitter.instruction("svc #0"); // Linux AArch64 trap (does not return) + } + emitter.instruction("b __rt_proc_open_fail"); // defensive fallthrough (never reached) + + // -- cleanup: close every parent_fd[j] and child_fd[j] for j < i where is_pipe[j] -- + emitter.label("__rt_proc_open_cleanup_sub"); + emitter.instruction("ldr x0, [sp, #72]"); // sub_box + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release sub_box before cleanup + emitter.instruction("b __rt_proc_open_cleanup"); // proceed to close opened pipes + emitter.label("__rt_proc_open_cleanup_m0"); + emitter.instruction("ldr x0, [sp, #88]"); // m0 + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release m0 + emitter.instruction("ldr x0, [sp, #72]"); // sub_box + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release sub_box + emitter.instruction("b __rt_proc_open_cleanup"); // proceed to close opened pipes + emitter.label("__rt_proc_open_cleanup_m1"); + emitter.instruction("ldr x0, [sp, #96]"); // m1 + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release m1 + emitter.instruction("ldr x0, [sp, #88]"); // m0 + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release m0 + emitter.instruction("ldr x0, [sp, #72]"); // sub_box + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release sub_box + emitter.instruction("b __rt_proc_open_cleanup"); // proceed to close opened pipes + emitter.label("__rt_proc_open_cleanup"); + emitter.instruction("str xzr, [sp, #400]"); // cleanup index j = 0 + emitter.label("__rt_proc_open_cleanup_test"); + emitter.instruction("ldr x9, [sp, #400]"); // reload cleanup j + emitter.instruction("ldr x10, [sp, #64]"); // bound = i (count of opened pipes) + emitter.instruction("cmp x9, x10"); // j < i? + emitter.instruction("b.ge __rt_proc_open_fail"); // all opened pipes closed -> fail + emitter.instruction("add x11, sp, #248"); // base of is_pipe + emitter.instruction("ldr x12, [x11, x9, lsl #3]"); // is_pipe[j] + emitter.instruction("cbz x12, __rt_proc_open_cleanup_next"); // not a pipe -> skip + emitter.instruction("add x11, sp, #120"); // base of parent_fd + emitter.instruction("ldr x0, [x11, x9, lsl #3]"); // parent_fd[j] + if is_macos { + emitter.instruction("mov x16, #6"); // macOS close syscall number (SYS_close=6) + emitter.instruction("svc #0x80"); // macOS AArch64 trap + } else { + emitter.instruction("mov x8, #57"); // Linux close syscall number + emitter.instruction("svc #0"); // Linux AArch64 trap + } + emitter.instruction("ldr x9, [sp, #400]"); // reload cleanup j (svc preserves x9) + emitter.instruction("add x11, sp, #184"); // base of child_fd + emitter.instruction("ldr x0, [x11, x9, lsl #3]"); // child_fd[j] + if is_macos { + emitter.instruction("mov x16, #6"); // macOS close syscall number (SYS_close=6) + emitter.instruction("svc #0x80"); // macOS AArch64 trap + } else { + emitter.instruction("mov x8, #57"); // Linux close syscall number + emitter.instruction("svc #0"); // Linux AArch64 trap + } + emitter.label("__rt_proc_open_cleanup_next"); + emitter.instruction("ldr x9, [sp, #400]"); // reload cleanup j + emitter.instruction("add x9, x9, #1"); // j += 1 + emitter.instruction("str x9, [sp, #400]"); // persist cleanup j + emitter.instruction("b __rt_proc_open_cleanup_test"); // continue cleanup + + // -- success: return the child pid -- + emitter.label("__rt_proc_open_done"); + emitter.instruction("ldr x0, [sp, #392]"); // reload the saved child pid + emitter.instruction("ldr x1, [sp, #40]"); // return the final pipes array/hash pointer beside the pid + emitter.instruction("ldp x29, x30, [sp, #0]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #496"); // release the frame + emitter.instruction("ret"); // return the pid (lowerer boxes it) + + // -- failure: return -1 (lowerer boxes as PHP false) -- + emitter.label("__rt_proc_open_fail"); + emitter.instruction("mov x0, #-1"); // report proc_open failure + emitter.instruction("ldr x1, [sp, #40]"); // return the original/current pipes container on failure + emitter.instruction("ldp x29, x30, [sp, #0]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #496"); // release the frame + emitter.instruction("ret"); // return the failure sentinel +} + +/// Emits the Linux-x86_64 `__rt_proc_open` runtime (System V AMD64). +/// +/// 480-byte `rbp`-relative frame. The kernel clobbers `rcx`/`r11` on syscall, +/// so loop counters live in frame slots and are reloaded after every trap. +/// Helper ABIs: `array_get_mixed_key(rdi/rsi/rdx/rcx -> rax)`, +/// `mixed_unbox(rax -> rax=tag, rdi=lo, rdx=hi)`, `str_eq(rdi/rsi/rdx/rcx -> rax)`, +/// `mixed_from_value(rax/rdi/rsi -> rax)`, `push_refcounted(rdi/rsi -> rax)`, +/// `decref_mixed(rax)`, `cstr(rax=ptr, rdx=len -> rax)`. +fn emit_proc_open_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: proc_open (C1b pipe-only) ---"); + emitter.label_global("__rt_proc_open"); + + // -- prologue: rbp frame, 480 bytes (16-byte aligned) -- + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish the helper frame pointer + emitter.instruction("sub rsp, 480"); // reserve the proc_open frame + emitter.instruction("mov QWORD PTR [rbp - 16], rdi"); // save the descriptor_spec array pointer + emitter.instruction("mov QWORD PTR [rbp - 24], rsi"); // save the command string pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rdx"); // save the command string length + emitter.instruction("mov QWORD PTR [rbp - 40], rcx"); // save the pipes array pointer + + // -- validate descriptor_spec: non-null + indexed kind + 1 <= n <= 8 -- + emitter.instruction("test rdi, rdi"); // null descriptor_spec? + emitter.instruction("jz __rt_proc_open_fail_x86"); // a null descriptor_spec is unrecoverable + emitter.instruction("mov r9, QWORD PTR [rdi - 8]"); // load the packed array kind metadata + emitter.instruction("and r9, 0xff"); // isolate the low-byte storage kind + emitter.instruction("cmp r9, 2"); // kind 2 = indexed array? + emitter.instruction("je __rt_proc_open_kind_ok_x86"); // accept indexed-array descriptor_spec + emitter.instruction("cmp r9, 3"); // kind 3 = hash storage? + emitter.instruction("jne __rt_proc_open_fail_x86"); // non-array descriptor_spec is unsupported + emitter.label("__rt_proc_open_kind_ok_x86"); + emitter.instruction("mov r9, QWORD PTR [rdi]"); // read the descriptor_spec length + emitter.instruction("test r9, r9"); // an empty descriptor spec is invalid + emitter.instruction("jz __rt_proc_open_fail_x86"); // bail out on an empty descriptor spec + emitter.instruction("cmp r9, 8"); // C1b bounds the descriptor count at 8 + emitter.instruction("ja __rt_proc_open_fail_x86"); // refuse an over-long descriptor spec + emitter.instruction("mov QWORD PTR [rbp - 48], r9"); // save the descriptor count n + emitter.instruction("mov r9, QWORD PTR [rdi - 8]"); // reload descriptor storage metadata after the count read + emitter.instruction("and r9, 0xff"); // isolate indexed/hash storage kind for position iteration + emitter.instruction("mov QWORD PTR [rbp - 480], r9"); // retain descriptor storage kind across runtime helper calls + emitter.instruction("mov QWORD PTR [rbp - 472], 0"); // start the hash insertion-order cursor at its head + + // -- zero the is_pipe bookkeeping array so cleanup is safe before any pipe opens -- + emitter.instruction("mov QWORD PTR [rbp - 400], 0"); // is_pipe[0] = 0 + emitter.instruction("mov QWORD PTR [rbp - 392], 0"); // is_pipe[1] = 0 + emitter.instruction("mov QWORD PTR [rbp - 384], 0"); // is_pipe[2] = 0 + emitter.instruction("mov QWORD PTR [rbp - 376], 0"); // is_pipe[3] = 0 + emitter.instruction("mov QWORD PTR [rbp - 368], 0"); // is_pipe[4] = 0 + emitter.instruction("mov QWORD PTR [rbp - 360], 0"); // is_pipe[5] = 0 + emitter.instruction("mov QWORD PTR [rbp - 352], 0"); // is_pipe[6] = 0 + emitter.instruction("mov QWORD PTR [rbp - 344], 0"); // is_pipe[7] = 0 + + // -- main descriptor loop: for i = 0 .. n-1, open one pipe per descriptor -- + emitter.instruction("mov QWORD PTR [rbp - 64], 0"); // i = 0 + emitter.label("__rt_proc_open_loop_test_x86"); + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // reload the loop index + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload n + emitter.instruction("cmp r9, r10"); // i < n? + emitter.instruction("jae __rt_proc_open_fork_x86"); // descriptor loop complete -> fork + + // -- resolve position i to its real integer descriptor key, then read it -- + emitter.instruction("cmp QWORD PTR [rbp - 480], 3"); // descriptor spec is associative hash storage? + emitter.instruction("jne __rt_proc_open_indexed_key_x86"); // indexed positions are their integer keys + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // hash descriptor-spec pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 472]"); // insertion-order hash cursor + abi::emit_call_label(emitter, "__rt_hash_iter_next"); + emitter.instruction("mov QWORD PTR [rbp - 472], rax"); // preserve the returned hash cursor + emitter.instruction("cmp rdx, -1"); // only integer descriptor keys are valid + emitter.instruction("jne __rt_proc_open_fail_x86"); // string keys cannot name child descriptors + emitter.instruction("mov QWORD PTR [rbp - 128], rdi"); // retain the actual descriptor integer key for this loop position + emitter.instruction("jmp __rt_proc_open_descriptor_key_ready_x86"); // skip indexed key synthesis + emitter.label("__rt_proc_open_indexed_key_x86"); + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // indexed position is its descriptor integer key + emitter.instruction("mov QWORD PTR [rbp - 128], r9"); // retain the descriptor integer key for this loop position + emitter.label("__rt_proc_open_descriptor_key_ready_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // descriptor_spec array pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 128]"); // key = actual descriptor integer key + emitter.instruction("mov rdx, -1"); // int-key sentinel + emitter.instruction("xor ecx, ecx"); // suppress missing-key warnings + abi::emit_call_label(emitter, "__rt_array_get_mixed_key"); + emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // save the owned sub_box across unbox + + // -- unbox sub_box: expect an indexed array (runtime tag 4) -- + abi::emit_call_label(emitter, "__rt_mixed_unbox"); // rax=tag, rdi=lo (sub ptr), rdx=hi + emitter.instruction("cmp rax, 4"); // tag 4 = indexed array? + emitter.instruction("jne __rt_proc_open_cleanup_sub_x86"); // non-array descriptor -> cleanup + fail + emitter.instruction("mov QWORD PTR [rbp - 80], rdi"); // save the sub-array pointer for element reads + + // -- read sub[0] (the descriptor type string) as an owned box -- + emitter.instruction("mov rdi, QWORD PTR [rbp - 80]"); // sub-array pointer + emitter.instruction("xor esi, esi"); // key = 0 + emitter.instruction("mov rdx, -1"); // int-key sentinel + emitter.instruction("xor ecx, ecx"); // suppress missing-key warnings + abi::emit_call_label(emitter, "__rt_array_get_mixed_key"); + emitter.instruction("mov QWORD PTR [rbp - 88], rax"); // save m0 (descriptor type string box) + + // -- unbox m0: expect a string (runtime tag 1) -- + abi::emit_call_label(emitter, "__rt_mixed_unbox"); // rax=tag, rdi=ptr, rdx=len + emitter.instruction("cmp rax, 1"); // tag 1 = string? + emitter.instruction("jne __rt_proc_open_cleanup_m0_x86"); // non-string descriptor type -> cleanup + fail + + // -- build the literal "pipe" on the stack and compare against sub[0] -- + emitter.instruction("mov BYTE PTR [rbp - 136], 0x70"); // lit_pipe[0] = 'p' + emitter.instruction("mov BYTE PTR [rbp - 135], 0x69"); // lit_pipe[1] = 'i' + emitter.instruction("mov BYTE PTR [rbp - 134], 0x70"); // lit_pipe[2] = 'p' + emitter.instruction("mov BYTE PTR [rbp - 133], 0x65"); // lit_pipe[3] = 'e' + // __rt_str_eq(ptr_a, len_a, ptr_b, len_b): rdi=ptr (from unbox), rsi=len (move from rdx) + emitter.instruction("mov rsi, rdx"); // len_a = string length from unbox + emitter.instruction("lea rdx, [rbp - 136]"); // ptr_b = &lit_pipe + emitter.instruction("mov rcx, 4"); // len_b = 4 + abi::emit_call_label(emitter, "__rt_str_eq"); // rax = 1 if "pipe" else 0 + emitter.instruction("test rax, rax"); // non-pipe descriptor unsupported in C1b? + emitter.instruction("jz __rt_proc_open_cleanup_m0_x86"); // non-pipe descriptor -> cleanup + fail + + // -- read sub[1] (the mode string) as an owned box -- + emitter.instruction("mov rdi, QWORD PTR [rbp - 80]"); // sub-array pointer + emitter.instruction("mov esi, 1"); // key = 1 + emitter.instruction("mov rdx, -1"); // int-key sentinel + emitter.instruction("xor ecx, ecx"); // suppress missing-key warnings + abi::emit_call_label(emitter, "__rt_array_get_mixed_key"); + emitter.instruction("mov QWORD PTR [rbp - 96], rax"); // save m1 (mode string box) + + // -- unbox m1: strings select their first byte; PHP scalar coercions read -- + abi::emit_call_label(emitter, "__rt_mixed_unbox"); // rax=tag, rdi=ptr, rdx=len + emitter.instruction("cmp rax, 1"); // tag 1 = string? + emitter.instruction("je __rt_proc_open_string_mode_x86"); // strings retain php-src's first-byte direction rule + emitter.instruction("cmp rax, 0"); // tag 0 = integer scalar? + emitter.instruction("je __rt_proc_open_scalar_mode_read_x86"); // decimal integer text cannot begin with `w` + emitter.instruction("cmp rax, 2"); // tag 2 = float scalar? + emitter.instruction("je __rt_proc_open_scalar_mode_read_x86"); // float text cannot begin with `w` + emitter.instruction("cmp rax, 3"); // tag 3 = boolean scalar? + emitter.instruction("je __rt_proc_open_scalar_mode_read_x86"); // boolean text cannot begin with `w` + emitter.instruction("cmp rax, 8"); // tag 8 = null scalar? + emitter.instruction("jne __rt_proc_open_cleanup_m1_x86"); // non-scalar mode remains unsupported by this runtime + emitter.label("__rt_proc_open_scalar_mode_read_x86"); + emitter.instruction("mov eax, 1"); // PHP scalar-to-string coercion produces a non-write-leading mode + emitter.instruction("jmp __rt_proc_open_mode_direction_ready_x86"); // release owned boxes using the common direction path + + // -- match php-src: only a leading "w" changes the pipe direction -- + emitter.label("__rt_proc_open_string_mode_x86"); + emitter.instruction("test rdx, rdx"); // mode string is empty? + emitter.instruction("jz __rt_proc_open_empty_mode_read_x86"); // empty modes use php-src's non-write/read direction + emitter.instruction("cmp BYTE PTR [rdi], 0x77"); // first byte is 'w'? + emitter.instruction("setne al"); // is_read = first byte is not 'w' + emitter.instruction("movzx rax, al"); // widen the direction flag for its stack slot + emitter.instruction("jmp __rt_proc_open_mode_direction_ready_x86"); // retain the computed non-empty mode direction + emitter.label("__rt_proc_open_empty_mode_read_x86"); + emitter.instruction("mov eax, 1"); // empty modes also make the child read + emitter.label("__rt_proc_open_mode_direction_ready_x86"); + emitter.instruction("mov QWORD PTR [rbp - 80], rax"); // save is_read in the sub_ptr slot (no longer needed) + + // -- release the three owned boxes now that is_read is known -- + emitter.instruction("mov rax, QWORD PTR [rbp - 88]"); // m0 + abi::emit_call_label(emitter, "__rt_decref_mixed"); // drop the caller's ref on m0 + emitter.instruction("mov rax, QWORD PTR [rbp - 96]"); // m1 + abi::emit_call_label(emitter, "__rt_decref_mixed"); // drop the caller's ref on m1 + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // sub_box + abi::emit_call_label(emitter, "__rt_decref_mixed"); // drop the caller's ref on sub_box + + // -- open a pipe pair: pipe2(&pipe_fds, 0) -- + emitter.instruction("lea rdi, [rbp - 104]"); // &pipe_fds[0] + emitter.instruction("xor esi, esi"); // pipe2 flags = 0 + emitter.instruction("mov eax, 293"); // Linux x86_64 pipe2 syscall number + emitter.instruction("syscall"); // Linux x86_64 trap (clobbers rcx/r11) + emitter.instruction("test rax, rax"); // pipe2 retval < 0 means failure + emitter.instruction("js __rt_proc_open_cleanup_x86"); // pipe open failed -> cleanup opened pipes + fail + + // -- record the read/write ends per mode: child gets the mode end, parent the other -- + emitter.instruction("mov r10d, DWORD PTR [rbp - 104]"); // read_end = pipe_fds[0] (32-bit int) + emitter.instruction("mov r11d, DWORD PTR [rbp - 100]"); // write_end = pipe_fds[1] (32-bit int) + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // i (descriptor index) + emitter.instruction("mov r8, QWORD PTR [rbp - 80]"); // reload is_read + emitter.instruction("test r8, r8"); // a write-leading mode gives the child write_end + emitter.instruction("jz __rt_proc_open_write_mode_x86"); // is_read == 0 -> write mode + emitter.instruction("lea rax, [rbp - 336]"); // base of child_fd + emitter.instruction("mov QWORD PTR [rax + r9 * 8], r10"); // non-write-leading mode gives the child read_end + emitter.instruction("lea rax, [rbp - 272]"); // base of parent_fd + emitter.instruction("mov QWORD PTR [rax + r9 * 8], r11"); // non-write-leading mode gives the parent write_end + emitter.instruction("jmp __rt_proc_open_pipe_recorded_x86"); // skip the write-mode assignment + emitter.label("__rt_proc_open_write_mode_x86"); + emitter.instruction("lea rax, [rbp - 336]"); // base of child_fd + emitter.instruction("mov QWORD PTR [rax + r9 * 8], r11"); // write-leading mode gives the child write_end + emitter.instruction("lea rax, [rbp - 272]"); // base of parent_fd + emitter.instruction("mov QWORD PTR [rax + r9 * 8], r10"); // write-leading mode gives the parent read_end + emitter.label("__rt_proc_open_pipe_recorded_x86"); + emitter.instruction("lea rax, [rbp - 400]"); // base of is_pipe + emitter.instruction("mov QWORD PTR [rax + r9 * 8], 1"); // is_pipe[i] = 1 + emitter.instruction("lea rax, [rbp - 464]"); // base of descriptor_index bookkeeping + emitter.instruction("mov r10, QWORD PTR [rbp - 128]"); // reload the real descriptor integer key + emitter.instruction("mov QWORD PTR [rax + r9 * 8], r10"); // descriptor_index[i] = real key for child/output phases + + // -- advance the loop index -- + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // reload i + emitter.instruction("inc r9"); // i += 1 + emitter.instruction("mov QWORD PTR [rbp - 64], r9"); // persist the loop index + emitter.instruction("jmp __rt_proc_open_loop_test_x86"); // continue the descriptor loop + + // -- fork: Linux x86_64 fork = 57 (no args) -- + emitter.label("__rt_proc_open_fork_x86"); + emitter.instruction("mov eax, 57"); // Linux x86_64 fork syscall number + emitter.instruction("syscall"); // Linux x86_64 trap (clobbers rcx/r11) + emitter.instruction("test rax, rax"); // fork retval < 0 means failure + emitter.instruction("js __rt_proc_open_cleanup_x86"); // fork failed -> close all opened pipes + fail + emitter.instruction("jz __rt_proc_open_child_x86"); // pid == 0 -> child branch + emitter.instruction("mov QWORD PTR [rbp - 120], rax"); // parent: save the child pid + + // -- parent: close every child end, then push the parent ends into $pipes -- + emitter.instruction("mov QWORD PTR [rbp - 64], 0"); // j = 0 + emitter.label("__rt_proc_open_parent_close_test_x86"); + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // reload j + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload n + emitter.instruction("cmp r9, r10"); // j < n? + emitter.instruction("jae __rt_proc_open_parent_push_x86"); // all child ends closed -> push phase + emitter.instruction("lea r11, [rbp - 400]"); // base of is_pipe + emitter.instruction("mov r8, QWORD PTR [r11 + r9 * 8]"); // is_pipe[j] + emitter.instruction("test r8, r8"); // not a pipe? + emitter.instruction("jz __rt_proc_open_parent_close_next_x86"); // skip non-pipe descriptors + emitter.instruction("lea r11, [rbp - 336]"); // base of child_fd + emitter.instruction("mov rdi, QWORD PTR [r11 + r9 * 8]"); // child_fd[j] + emitter.instruction("mov eax, 3"); // Linux x86_64 close syscall number + emitter.instruction("syscall"); // close the child end (clobbers rcx/r11) + emitter.label("__rt_proc_open_parent_close_next_x86"); + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // reload j + emitter.instruction("inc r9"); // j += 1 + emitter.instruction("mov QWORD PTR [rbp - 64], r9"); // persist j + emitter.instruction("jmp __rt_proc_open_parent_close_test_x86"); // continue closing child ends + + // -- parent publish phase: write each parent end at its descriptor key -- + emitter.label("__rt_proc_open_parent_push_x86"); + emitter.instruction("mov QWORD PTR [rbp - 64], 0"); // j = 0 + emitter.instruction("mov QWORD PTR [rbp - 56], 0"); // pipe_count = 0 + emitter.label("__rt_proc_open_parent_push_test_x86"); + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // reload j + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload n + emitter.instruction("cmp r9, r10"); // j < n? + emitter.instruction("jae __rt_proc_open_done_x86"); // all pipes pushed -> return pid + emitter.instruction("lea r11, [rbp - 400]"); // base of is_pipe + emitter.instruction("mov r8, QWORD PTR [r11 + r9 * 8]"); // is_pipe[j] + emitter.instruction("test r8, r8"); // not a pipe? + emitter.instruction("jz __rt_proc_open_parent_push_next_x86"); // skip non-pipe descriptors + emitter.instruction("lea r11, [rbp - 272]"); // base of parent_fd + emitter.instruction("mov rdi, QWORD PTR [r11 + r9 * 8]"); // parent_fd[j] (resource handle lo) + emitter.instruction("mov rax, 9"); // tag 9 = resource + emitter.instruction("mov rsi, 1"); // hi = kind 1 (native stream fd) + abi::emit_call_label(emitter, "__rt_mixed_from_value"); // rax = res_box (owned, refcount 1) + emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // save the owned resource box for the keyed container write + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // reload j after the allocating Mixed helper clobbered caller-saved registers + emitter.instruction("lea r11, [rbp - 464]"); // base of descriptor_index bookkeeping + emitter.instruction("mov rdi, QWORD PTR [r11 + r9 * 8]"); // real integer descriptor key for this parent pipe + emitter.instruction("xor esi, esi"); // integer key has no high payload word + emitter.instruction("xor eax, eax"); // tag 0 = integer key Mixed cell + abi::emit_call_label(emitter, "__rt_mixed_from_value"); // allocate the borrowed key cell used by the generic setter + emitter.instruction("mov QWORD PTR [rbp - 88], rax"); // retain the key cell across the container write + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload the current pipes array/hash pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 88]"); // pass the integer descriptor key cell + emitter.instruction("mov rdx, QWORD PTR [rbp - 72]"); // transfer the resource box into the destination container + abi::emit_call_label(emitter, "__rt_array_set_mixed_key"); // set resource at the real descriptor key, returning promoted/reallocated container + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // persist the potentially promoted/reallocated pipes container + emitter.instruction("mov rax, QWORD PTR [rbp - 88]"); // reload the borrowed key cell after the setter read it + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the caller-owned key box; the setter never owns keys + emitter.label("__rt_proc_open_parent_push_next_x86"); + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // reload j + emitter.instruction("inc r9"); // j += 1 + emitter.instruction("mov QWORD PTR [rbp - 64], r9"); // persist j + emitter.instruction("jmp __rt_proc_open_parent_push_test_x86"); // continue pushing parent ends + + // -- child: dup2 each child end onto descriptor fd i, close the parent ends, execve -- + emitter.label("__rt_proc_open_child_x86"); + emitter.instruction("mov QWORD PTR [rbp - 64], 0"); // j = 0 + emitter.label("__rt_proc_open_child_dup_test_x86"); + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // reload j + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload n + emitter.instruction("cmp r9, r10"); // j < n? + emitter.instruction("jae __rt_proc_open_child_close_phase_x86"); // all dups done -> close parent ends + emitter.instruction("lea r11, [rbp - 400]"); // base of is_pipe + emitter.instruction("mov r8, QWORD PTR [r11 + r9 * 8]"); // is_pipe[j] + emitter.instruction("test r8, r8"); // not a pipe? + emitter.instruction("jz __rt_proc_open_child_dup_next_x86"); // skip non-pipe descriptors + emitter.instruction("lea r11, [rbp - 336]"); // base of child_fd + emitter.instruction("mov rdi, QWORD PTR [r11 + r9 * 8]"); // child_fd[j] (old fd) + emitter.instruction("lea r11, [rbp - 464]"); // base of descriptor_index bookkeeping + emitter.instruction("mov rsi, QWORD PTR [r11 + r9 * 8]"); // new fd = real descriptor integer key + emitter.instruction("mov eax, 33"); // Linux x86_64 dup2 syscall number + emitter.instruction("syscall"); // dup2 (clobbers rcx/r11) + // -- if child_fd[j] != j, close the now-redundant original child end -- + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // reload j (rcx clobbered by syscall) + emitter.instruction("lea r11, [rbp - 336]"); // base of child_fd + emitter.instruction("mov r8, QWORD PTR [r11 + r9 * 8]"); // reload child_fd[j] + emitter.instruction("lea r11, [rbp - 464]"); // base of descriptor_index bookkeeping + emitter.instruction("mov r10, QWORD PTR [r11 + r9 * 8]"); // reload the real descriptor integer key + emitter.instruction("cmp r8, r10"); // child_fd[j] == descriptor key? + emitter.instruction("je __rt_proc_open_child_dup_next_x86"); // equal -> no close needed (fd is already the dup target) + emitter.instruction("mov rdi, r8"); // close(child_fd[j]) + emitter.instruction("mov eax, 3"); // Linux x86_64 close syscall number + emitter.instruction("syscall"); // close the redundant child end + emitter.label("__rt_proc_open_child_dup_next_x86"); + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // reload j + emitter.instruction("inc r9"); // j += 1 + emitter.instruction("mov QWORD PTR [rbp - 64], r9"); // persist j + emitter.instruction("jmp __rt_proc_open_child_dup_test_x86"); // continue the dup loop + + // -- child: close every parent end so the child does not hold the parent's pipe side -- + emitter.label("__rt_proc_open_child_close_phase_x86"); + emitter.instruction("mov QWORD PTR [rbp - 64], 0"); // j = 0 + emitter.label("__rt_proc_open_child_close_test_x86"); + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // reload j + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload n + emitter.instruction("cmp r9, r10"); // j < n? + emitter.instruction("jae __rt_proc_open_child_exec_x86"); // all parent ends closed -> execve + emitter.instruction("lea r11, [rbp - 400]"); // base of is_pipe + emitter.instruction("mov r8, QWORD PTR [r11 + r9 * 8]"); // is_pipe[j] + emitter.instruction("test r8, r8"); // not a pipe? + emitter.instruction("jz __rt_proc_open_child_close_next_x86"); // skip non-pipe descriptors + emitter.instruction("lea r11, [rbp - 272]"); // base of parent_fd + emitter.instruction("mov rdi, QWORD PTR [r11 + r9 * 8]"); // parent_fd[j] + emitter.instruction("mov eax, 3"); // Linux x86_64 close syscall number + emitter.instruction("syscall"); // close the parent end + emitter.label("__rt_proc_open_child_close_next_x86"); + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // reload j + emitter.instruction("inc r9"); // j += 1 + emitter.instruction("mov QWORD PTR [rbp - 64], r9"); // persist j + emitter.instruction("jmp __rt_proc_open_child_close_test_x86"); // continue closing parent ends + + // -- child: build the execve payload and exec /bin/sh -c -- + emitter.label("__rt_proc_open_child_exec_x86"); + // __rt_cstr takes ptr in rax and len in rdx; command lives in rsi/rdx currently + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // command pointer into __rt_cstr input + emitter.instruction("mov rdx, QWORD PTR [rbp - 32]"); // command length into __rt_cstr input + abi::emit_call_label(emitter, "__rt_cstr"); // rax = null-terminated command string + emitter.instruction("mov QWORD PTR [rbp - 176], rax"); // save cmd_cstr for argv[2] + // -- store "/bin/sh\0" into path_buf -- + emitter.instruction("mov BYTE PTR [rbp - 152], 0x2f"); // path_buf[0] = '/' + emitter.instruction("mov BYTE PTR [rbp - 151], 0x62"); // path_buf[1] = 'b' + emitter.instruction("mov BYTE PTR [rbp - 150], 0x69"); // path_buf[2] = 'i' + emitter.instruction("mov BYTE PTR [rbp - 149], 0x6e"); // path_buf[3] = 'n' + emitter.instruction("mov BYTE PTR [rbp - 148], 0x2f"); // path_buf[4] = '/' + emitter.instruction("mov BYTE PTR [rbp - 147], 0x73"); // path_buf[5] = 's' + emitter.instruction("mov BYTE PTR [rbp - 146], 0x68"); // path_buf[6] = 'h' + emitter.instruction("mov BYTE PTR [rbp - 145], 0"); // path_buf[7] = NUL + // -- store "sh\0" into argv0 -- + emitter.instruction("mov BYTE PTR [rbp - 160], 0x73"); // argv0[0] = 's' + emitter.instruction("mov BYTE PTR [rbp - 159], 0x68"); // argv0[1] = 'h' + emitter.instruction("mov BYTE PTR [rbp - 158], 0"); // argv0[2] = NUL + // -- store "-c\0" into argv1 -- + emitter.instruction("mov BYTE PTR [rbp - 168], 0x2d"); // argv1[0] = '-' + emitter.instruction("mov BYTE PTR [rbp - 167], 0x63"); // argv1[1] = 'c' + emitter.instruction("mov BYTE PTR [rbp - 166], 0"); // argv1[2] = NUL + // -- build argv[4] = { &argv0, &argv1, cmd_cstr, NULL } -- + emitter.instruction("lea r9, [rbp - 160]"); // &argv0 + emitter.instruction("mov QWORD PTR [rbp - 208], r9"); // argv[0] = &argv0 + emitter.instruction("lea r9, [rbp - 168]"); // &argv1 + emitter.instruction("mov QWORD PTR [rbp - 200], r9"); // argv[1] = &argv1 + emitter.instruction("mov r9, QWORD PTR [rbp - 176]"); // cmd_cstr + emitter.instruction("mov QWORD PTR [rbp - 192], r9"); // argv[2] = cmd_cstr + emitter.instruction("mov QWORD PTR [rbp - 184], 0"); // argv[3] = NULL + // -- execve(path_buf, argv, NULL) -- + emitter.instruction("lea rdi, [rbp - 152]"); // path = &path_buf + emitter.instruction("lea rsi, [rbp - 208]"); // argv = &argv[0] + emitter.instruction("xor edx, edx"); // envp = NULL + emitter.instruction("mov eax, 59"); // Linux x86_64 execve syscall number + emitter.instruction("syscall"); // execve (does not return on success) + // -- execve returned (failure): exit the child with status 127 -- + emitter.instruction("mov edi, 127"); // child exit status for execve failure + emitter.instruction("mov eax, 60"); // Linux x86_64 exit syscall number + emitter.instruction("syscall"); // exit the child (does not return) + emitter.instruction("jmp __rt_proc_open_fail_x86"); // defensive fallthrough (never reached) + + // -- cleanup: release owned boxes for the mid-loop failure paths -- + emitter.label("__rt_proc_open_cleanup_sub_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // sub_box + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release sub_box before cleanup + emitter.instruction("jmp __rt_proc_open_cleanup_x86"); // proceed to close opened pipes + emitter.label("__rt_proc_open_cleanup_m0_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 88]"); // m0 + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release m0 + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // sub_box + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release sub_box + emitter.instruction("jmp __rt_proc_open_cleanup_x86"); // proceed to close opened pipes + emitter.label("__rt_proc_open_cleanup_m1_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 96]"); // m1 + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release m1 + emitter.instruction("mov rax, QWORD PTR [rbp - 88]"); // m0 + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release m0 + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // sub_box + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release sub_box + emitter.instruction("jmp __rt_proc_open_cleanup_x86"); // proceed to close opened pipes + emitter.label("__rt_proc_open_cleanup_x86"); + emitter.instruction("mov QWORD PTR [rbp - 128], 0"); // cleanup index j = 0 + emitter.label("__rt_proc_open_cleanup_test_x86"); + emitter.instruction("mov r9, QWORD PTR [rbp - 128]"); // reload cleanup j + emitter.instruction("mov r10, QWORD PTR [rbp - 64]"); // bound = i (count of opened pipes) + emitter.instruction("cmp r9, r10"); // j < i? + emitter.instruction("jae __rt_proc_open_fail_x86"); // all opened pipes closed -> fail + emitter.instruction("lea r11, [rbp - 400]"); // base of is_pipe + emitter.instruction("mov r8, QWORD PTR [r11 + r9 * 8]"); // is_pipe[j] + emitter.instruction("test r8, r8"); // not a pipe? + emitter.instruction("jz __rt_proc_open_cleanup_next_x86"); // skip non-pipe descriptors + emitter.instruction("lea r11, [rbp - 272]"); // base of parent_fd + emitter.instruction("mov rdi, QWORD PTR [r11 + r9 * 8]"); // parent_fd[j] + emitter.instruction("mov eax, 3"); // Linux x86_64 close syscall number + emitter.instruction("syscall"); // close parent_fd[j] + emitter.instruction("mov r9, QWORD PTR [rbp - 128]"); // reload cleanup j (rcx clobbered) + emitter.instruction("lea r11, [rbp - 336]"); // base of child_fd + emitter.instruction("mov rdi, QWORD PTR [r11 + r9 * 8]"); // child_fd[j] + emitter.instruction("mov eax, 3"); // Linux x86_64 close syscall number + emitter.instruction("syscall"); // close child_fd[j] + emitter.label("__rt_proc_open_cleanup_next_x86"); + emitter.instruction("mov r9, QWORD PTR [rbp - 128]"); // reload cleanup j + emitter.instruction("inc r9"); // j += 1 + emitter.instruction("mov QWORD PTR [rbp - 128], r9"); // persist cleanup j + emitter.instruction("jmp __rt_proc_open_cleanup_test_x86"); // continue cleanup + + // -- success: return the child pid -- + emitter.label("__rt_proc_open_done_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 120]"); // reload the saved child pid + emitter.instruction("mov rdx, QWORD PTR [rbp - 40]"); // return the final pipes array/hash pointer beside the pid + emitter.instruction("add rsp, 480"); // release the frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the pid (lowerer boxes it) + + // -- failure: return -1 (lowerer boxes as PHP false) -- + emitter.label("__rt_proc_open_fail_x86"); + emitter.instruction("mov rax, -1"); // report proc_open failure + emitter.instruction("mov rdx, QWORD PTR [rbp - 40]"); // return the original/current pipes container on failure + emitter.instruction("add rsp, 480"); // release the frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the failure sentinel +} + +/// Emits the Windows-x86_64 `__rt_proc_open` runtime (real C1c: `CreatePipe` + +/// `CreateProcessW`, MSx64 ABI). +/// +/// 896-byte `rbp`-relative frame (persistent data in `[16, 800)`, the last 96 +/// bytes `[800, 896)` are outgoing-call shadow space + stack args, addressed +/// via `[rsp + K]`). Offsets from `rbp`: `[16]` desc, `[24]` cmd_ptr, `[32]` +/// cmd_len, `[40]` pipes, `[48]` n, `[56]` scratch (cmdbuf total size late in +/// the function; unused earlier), `[64]` i/j (reused per phase), `[72]` +/// sub_box, `[80]` sub_ptr (reused for `is_read`), `[88]` m0, `[96]` m1, +/// `[104]`/`[112]` CreatePipe read_end/write_end, `[120]` lit_pipe, `[128]` +/// lit_r, `[136]` cmdbuf pointer, `[144]`/`[152]`/`[160]` NUL handles for +/// stdin/stdout/stderr, `[168, 232)` parent_handle\[8\] (element `j` at +/// `rbp - 224 + 8*j`; raw HANDLEs until publication, then CRT fds), +/// `[232, 296)` child_handle\[8\] (element `j` at `rbp - 288 + 8*j`), +/// `[296, 360)` pipe_state\[8\] (element `j` at `rbp - 352 + 8*j`; bit 0 +/// means pipe open, bit 1 means parent-readable, bit 2 means CRT-adopted, +/// bit 3 means raw Winsock socket pair, and bit 4 means child-only handle), +/// `[360, 384)` SECURITY_ATTRIBUTES, `[384, 488)` +/// STARTUPINFOW, `[488, 512)` PROCESS_INFORMATION, `[512, 520)` a 4-byte +/// "NUL\0" literal (reused as the cleanup-loop counter, slot name +/// `cleanup_j`, once the descriptor loop and NUL redirection are behind us), +/// `[528]` cwd pointer, `[536]` cwd length, `[544]` owned narrow cwd staging, +/// `[552]` owned UTF-16 cwd, `[616]` saved process error mode, and `[624]` +/// temporary `GetLastError()` result. The transported env/options pointers occupy +/// SysV stack arguments 7/8; options use the five low bits of the packed flag +/// word and environment length begins at bit five. `[728, 792)` retains the +/// eight real descriptor integer keys, `[792]` is the associative iterator +/// cursor, `[800]` is the descriptor storage kind, and `[704]`/`[712]` retain +/// a temporary NUL-terminated UTF-8 file path and its allocation size. Keeping those slots +/// below the environment state prevents descriptor key 7 from aliasing the +/// owned UTF-16 environment pointer. +/// +/// Descriptor-spec parsing validates real integer keys and accepts `pipe`, +/// `null`, `redirect`, `file`, and stream-resource descriptors. `pipe` owns a +/// parent endpoint that is later converted to a CRT fd and published in `$pipes`; +/// all other descriptors only own the inheritable child handle and therefore +/// close it immediately after `CreateProcessW` succeeds. File paths are strictly +/// converted from UTF-8 and opened with `CreateFileW`; stream resources use +/// `_get_osfhandle` followed by `DuplicateHandle` so the caller retains ownership. +/// `socket` uses a private AF_INET loopback pair so the public AF_UNIX +/// `socketpair` API remains unsupported; `pty` remains outside this helper. +/// Parsing (validate / unbox / `"pipe"`/`"r"` compare / +/// decref) is a byte-for-byte copy of `emit_proc_open_linux_x86_64`'s SysV +/// internal-helper calls (`rdi`/`rsi`/`rdx`/`rcx`), since those helpers are +/// emitted once for x86_64 and shared by every platform (see +/// `array_get_mixed_key.rs`, `mixed_unbox.rs`, `str_eq.rs`, +/// `mixed_from_value.rs`, `array_push_refcounted.rs`, `decref_mixed.rs`). +/// Only the pipe/spawn mechanism differs: `CreatePipe` replaces `pipe2`, +/// `STARTUPINFOW.hStd*` replaces `dup2`, and `CreateProcessW` replaces +/// `fork`/`execve`. No register is ever trusted to survive a `call` (SysV +/// helper or Win32 API): every value is reloaded from its `rbp`-relative slot +/// immediately afterward, since Win32 volatile registers (`rax`/`rcx`/`rdx`/ +/// `r8`-`r11`) differ from the SysV internal-helper convention. +/// +/// Mode mapping mirrors php-src: a leading `w` (the child writes, e.g. +/// stdout/stderr) is the reverse; every other first byte (including `r`/`rb`) +/// puts the pipe's read end in the child and the write end in the parent. Only descriptor +/// indices 0/1/2 are wired into `STARTUPINFOW`; indices `>= 3` still get a +/// real, inheritable pipe end pushed into `$pipes`, but the child has no +/// numbered-fd convention to receive it on Windows (documented C1c +/// limitation, consistent with C1b's own descriptor-count bound). +fn emit_proc_open_win32_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: proc_open (C1c: CreatePipe/CreateProcessW) ---"); + emitter.label_global("__rt_proc_open"); + + // -- prologue: rbp frame, 896 bytes (persistent 800B + 96B call scratch) -- + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish the helper frame pointer + emitter.instruction("sub rsp, 896"); // reserve persistent state, descriptor keys, and MSx64 call scratch + emitter.instruction("mov QWORD PTR [rbp - 16], rdi"); // save the descriptor_spec array pointer + emitter.instruction("mov QWORD PTR [rbp - 24], rsi"); // save the command string pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rdx"); // save the command string length + emitter.instruction("mov QWORD PTR [rbp - 40], rcx"); // save the pipes array pointer + emitter.instruction("mov QWORD PTR [rbp - 528], r8"); // save optional cwd string pointer + emitter.instruction("mov QWORD PTR [rbp - 536], r9"); // save optional cwd string length + emitter.instruction("mov QWORD PTR [rbp - 544], 0"); // cwd narrow staging buffer = NULL + emitter.instruction("mov QWORD PTR [rbp - 552], 0"); // cwd UTF-16 buffer = NULL + emitter.instruction("mov rax, QWORD PTR [rbp + 16]"); // load arg7: optional UTF-8 environment block pointer + emitter.instruction("mov QWORD PTR [rbp - 560], rax"); // preserve the environment block pointer + emitter.instruction("mov rax, QWORD PTR [rbp + 24]"); // load arg8: packed environment length/direct flag + emitter.instruction("mov QWORD PTR [rbp - 568], rax"); // preserve the packed proc_open flags + emitter.instruction("mov QWORD PTR [rbp - 576], 0"); // owned UTF-16 environment block = none yet + emitter.instruction("mov QWORD PTR [rbp - 584], 0"); // command-line prefix length = direct by default + emitter.instruction("mov QWORD PTR [rbp - 592], 0"); // command-line suffix length = direct by default + emitter.instruction("mov QWORD PTR [rbp - 616], 0"); // saved SetErrorMode value when suppression is enabled + + // -- reject an invalid dynamic options marker before acquiring resources -- + emitter.instruction("mov rax, QWORD PTR [rbp - 568]"); // reload the packed marshalling flags + emitter.instruction("test rax, rax"); // inspect the invalid-options sign bit + emitter.instruction("js __rt_proc_open_fail_win"); // runtime option validation already published errno + + // -- validate descriptor_spec: non-null + indexed kind + 1 <= n <= 8 -- + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // reload the descriptor_spec array pointer + emitter.instruction("test rdi, rdi"); // null descriptor_spec? + emitter.instruction("jz __rt_proc_open_fail_win"); // a null descriptor_spec is unrecoverable + emitter.instruction("mov r9, QWORD PTR [rdi - 8]"); // load the packed array kind metadata + emitter.instruction("and r9, 0xff"); // isolate the low-byte storage kind + emitter.instruction("cmp r9, 2"); // kind 2 = indexed array? + emitter.instruction("je __rt_proc_open_kind_ok_win"); // accept indexed-array descriptor_spec + emitter.instruction("cmp r9, 3"); // kind 3 = hash storage? + emitter.instruction("jne __rt_proc_open_fail_win"); // non-array descriptor_spec is unsupported + emitter.label("__rt_proc_open_kind_ok_win"); + emitter.instruction("mov r9, QWORD PTR [rdi]"); // read the descriptor_spec length + emitter.instruction("test r9, r9"); // an empty descriptor spec is invalid + emitter.instruction("jz __rt_proc_open_fail_win"); // bail out on an empty descriptor spec + emitter.instruction("cmp r9, 8"); // C1c bounds the descriptor count at 8 + emitter.instruction("ja __rt_proc_open_fail_win"); // refuse an over-long descriptor spec + emitter.instruction("mov QWORD PTR [rbp - 48], r9"); // save the descriptor count n + emitter.instruction("mov r9, QWORD PTR [rdi - 8]"); // reload descriptor storage metadata after reading n + emitter.instruction("and r9, 0xff"); // isolate indexed/hash storage kind for position iteration + emitter.instruction("mov QWORD PTR [rbp - 800], r9"); // retain descriptor storage kind across runtime helper calls + emitter.instruction("mov QWORD PTR [rbp - 792], 0"); // begin associative iteration at its insertion-order cursor + + // -- zero the is_pipe bookkeeping array so cleanup is safe before any pipe opens -- + emitter.instruction("mov QWORD PTR [rbp - 296], 0"); // is_pipe[0] = 0 + emitter.instruction("mov QWORD PTR [rbp - 304], 0"); // is_pipe[1] = 0 + emitter.instruction("mov QWORD PTR [rbp - 312], 0"); // is_pipe[2] = 0 + emitter.instruction("mov QWORD PTR [rbp - 320], 0"); // is_pipe[3] = 0 + emitter.instruction("mov QWORD PTR [rbp - 328], 0"); // is_pipe[4] = 0 + emitter.instruction("mov QWORD PTR [rbp - 336], 0"); // is_pipe[5] = 0 + emitter.instruction("mov QWORD PTR [rbp - 344], 0"); // is_pipe[6] = 0 + emitter.instruction("mov QWORD PTR [rbp - 352], 0"); // is_pipe[7] = 0 + + // -- zero the NUL-handle and cmdbuf slots so failure cleanup can safely skip them -- + emitter.instruction("mov QWORD PTR [rbp - 144], 0"); // nul_handle[0] (stdin) = none yet + emitter.instruction("mov QWORD PTR [rbp - 152], 0"); // nul_handle[1] (stdout) = none yet + emitter.instruction("mov QWORD PTR [rbp - 160], 0"); // nul_handle[2] (stderr) = none yet + emitter.instruction("mov QWORD PTR [rbp - 136], 0"); // cmdbuf = NULL (not yet allocated) + emitter.instruction("mov QWORD PTR [rbp - 56], 0"); // wide_cmdbuf = NULL (not yet converted) + + // -- build the reusable heritable SECURITY_ATTRIBUTES (sa): 24 bytes -- + emitter.instruction("mov QWORD PTR [rbp - 384], 24"); // sa.nLength = sizeof(SECURITY_ATTRIBUTES) + emitter.instruction("mov QWORD PTR [rbp - 376], 0"); // sa.lpSecurityDescriptor = NULL + emitter.instruction("mov QWORD PTR [rbp - 368], 1"); // sa.bInheritHandle = TRUE (pipe ends are born inheritable) + + // -- zero-init STARTUPINFOW (104 bytes: 13 QWORDs), then set cb and dwFlags -- + emitter.instruction("mov QWORD PTR [rbp - 488], 0"); // si bytes [0, 8) = 0 (cb + reserved) + emitter.instruction("mov QWORD PTR [rbp - 392], 0"); // si bytes [96, 104) = 0 + emitter.instruction("mov QWORD PTR [rbp - 400], 0"); // si bytes [88, 96) = 0 + emitter.instruction("mov QWORD PTR [rbp - 408], 0"); // si bytes [80, 88) = 0 + emitter.instruction("mov QWORD PTR [rbp - 416], 0"); // si bytes [72, 80) = 0 + emitter.instruction("mov QWORD PTR [rbp - 424], 0"); // si bytes [64, 72) = 0 + emitter.instruction("mov QWORD PTR [rbp - 432], 0"); // si bytes [56, 64) = 0 (includes dwFlags) + emitter.instruction("mov QWORD PTR [rbp - 440], 0"); // si bytes [48, 56) = 0 + emitter.instruction("mov QWORD PTR [rbp - 448], 0"); // si bytes [40, 48) = 0 + emitter.instruction("mov QWORD PTR [rbp - 456], 0"); // si bytes [32, 40) = 0 + emitter.instruction("mov QWORD PTR [rbp - 464], 0"); // si bytes [24, 32) = 0 + emitter.instruction("mov QWORD PTR [rbp - 472], 0"); // si bytes [16, 24) = 0 + emitter.instruction("mov QWORD PTR [rbp - 480], 0"); // si bytes [8, 16) = 0 + emitter.instruction("mov DWORD PTR [rbp - 488], 104"); // si.cb = sizeof(STARTUPINFOW) + emitter.instruction("mov DWORD PTR [rbp - 428], 0x100"); // si.dwFlags = STARTF_USESTDHANDLES + + // -- main descriptor loop: for i = 0 .. n-1, open one pipe per descriptor -- + emitter.instruction("mov QWORD PTR [rbp - 64], 0"); // i = 0 + emitter.label("__rt_proc_open_loop_test_win"); + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // reload the loop index + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload n + emitter.instruction("cmp r9, r10"); // i < n? + emitter.instruction("jae __rt_proc_open_nul_fill_win"); // descriptor loop complete -> fill unwired std handles + + // -- resolve position i to its real integer descriptor key, then read it -- + emitter.instruction("cmp QWORD PTR [rbp - 800], 3"); // descriptor spec uses associative hash storage? + emitter.instruction("jne __rt_proc_open_indexed_key_win"); // packed positions are their integer keys + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // associative descriptor-spec pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 792]"); // insertion-order hash cursor + abi::emit_call_label(emitter, "__rt_hash_iter_next"); + emitter.instruction("mov QWORD PTR [rbp - 792], rax"); // preserve the returned hash cursor + emitter.instruction("cmp rdx, -1"); // only integer descriptor keys are supported by proc_open + emitter.instruction("jne __rt_proc_open_fail_win"); // reject string descriptor keys before opening a pipe + emitter.instruction("mov r10, QWORD PTR [rbp - 64]"); // reload position after the iterator call + emitter.instruction("lea r11, [rbp - 784]"); // base of real descriptor-key bookkeeping + emitter.instruction("mov QWORD PTR [r11 + r10 * 8], rdi"); // retain this actual integer descriptor key + emitter.instruction("jmp __rt_proc_open_descriptor_key_ready_win"); // skip packed-key synthesis + emitter.label("__rt_proc_open_indexed_key_win"); + emitter.instruction("mov r10, QWORD PTR [rbp - 64]"); // packed position is its descriptor key + emitter.instruction("lea r11, [rbp - 784]"); // base of real descriptor-key bookkeeping + emitter.instruction("mov QWORD PTR [r11 + r10 * 8], r10"); // retain the packed integer descriptor key + emitter.label("__rt_proc_open_descriptor_key_ready_win"); + emitter.instruction("mov r10, QWORD PTR [rbp - 64]"); // reload position for the keyed mixed lookup + emitter.instruction("lea r11, [rbp - 784]"); // base of real descriptor-key bookkeeping + emitter.instruction("mov rsi, QWORD PTR [r11 + r10 * 8]"); // key = actual descriptor integer key + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // descriptor_spec array pointer + emitter.instruction("mov rdx, -1"); // int-key sentinel + emitter.instruction("xor ecx, ecx"); // suppress missing-key warnings + abi::emit_call_label(emitter, "__rt_array_get_mixed_key"); + emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // save the owned sub_box across unbox + + // -- unbox sub_box: an indexed descriptor array or a stream resource -- + abi::emit_call_label(emitter, "__rt_mixed_unbox"); // rax=tag, rdi=lo (sub ptr), rdx=hi + emitter.instruction("cmp rax, 9"); // tag 9 = resource descriptor? + emitter.instruction("je __rt_proc_open_resource_descriptor_win"); // duplicate a stream resource for the child + emitter.instruction("cmp rax, 4"); // tag 4 = indexed array? + emitter.instruction("jne __rt_proc_open_cleanup_sub_win"); // non-array descriptor -> cleanup + fail + emitter.instruction("mov QWORD PTR [rbp - 80], rdi"); // save the sub-array pointer for element reads + + // -- read sub[0] (the descriptor type string) as an owned box -- + emitter.instruction("mov rdi, QWORD PTR [rbp - 80]"); // sub-array pointer + emitter.instruction("xor esi, esi"); // key = 0 + emitter.instruction("mov rdx, -1"); // int-key sentinel + emitter.instruction("xor ecx, ecx"); // suppress missing-key warnings + abi::emit_call_label(emitter, "__rt_array_get_mixed_key"); + emitter.instruction("mov QWORD PTR [rbp - 88], rax"); // save m0 (descriptor type string box) + + // -- unbox m0: expect a string (runtime tag 1) -- + abi::emit_call_label(emitter, "__rt_mixed_unbox"); // rax=tag, rdi=ptr, rdx=len + emitter.instruction("cmp rax, 1"); // tag 1 = string? + emitter.instruction("jne __rt_proc_open_cleanup_m0_win"); // non-string descriptor type -> cleanup + fail + + // -- build the literal "pipe" on the stack and compare against sub[0] -- + emitter.instruction("mov BYTE PTR [rbp - 120], 0x70"); // lit_pipe[0] = 'p' + emitter.instruction("mov BYTE PTR [rbp - 119], 0x69"); // lit_pipe[1] = 'i' + emitter.instruction("mov BYTE PTR [rbp - 118], 0x70"); // lit_pipe[2] = 'p' + emitter.instruction("mov BYTE PTR [rbp - 117], 0x65"); // lit_pipe[3] = 'e' + // __rt_str_eq(ptr_a, len_a, ptr_b, len_b): rdi=ptr (from unbox), rsi=len (move from rdx) + emitter.instruction("mov rsi, rdx"); // len_a = string length from unbox + emitter.instruction("lea rdx, [rbp - 120]"); // ptr_b = &lit_pipe + emitter.instruction("mov rcx, 4"); // len_b = 4 + abi::emit_call_label(emitter, "__rt_str_eq"); // rax = 1 if "pipe" else 0 + emitter.instruction("test rax, rax"); // did the type match pipe? + emitter.instruction("jz __rt_proc_open_nonpipe_descriptor_win"); // dispatch another PHP descriptor kind + + // -- read sub[1] (the mode string) as an owned box -- + emitter.instruction("mov rdi, QWORD PTR [rbp - 80]"); // sub-array pointer + emitter.instruction("mov esi, 1"); // key = 1 + emitter.instruction("mov rdx, -1"); // int-key sentinel + emitter.instruction("xor ecx, ecx"); // suppress missing-key warnings + abi::emit_call_label(emitter, "__rt_array_get_mixed_key"); + emitter.instruction("mov QWORD PTR [rbp - 96], rax"); // save m1 (mode string box) + + // -- unbox m1: strings select their first byte; PHP scalar coercions read -- + abi::emit_call_label(emitter, "__rt_mixed_unbox"); // rax=tag, rdi=ptr, rdx=len + emitter.instruction("cmp rax, 1"); // tag 1 = string? + emitter.instruction("je __rt_proc_open_string_mode_win"); // strings retain php-src's first-byte direction rule + emitter.instruction("cmp rax, 0"); // tag 0 = integer scalar? + emitter.instruction("je __rt_proc_open_scalar_mode_read_win"); // decimal integer text cannot begin with `w` + emitter.instruction("cmp rax, 2"); // tag 2 = float scalar? + emitter.instruction("je __rt_proc_open_scalar_mode_read_win"); // float text cannot begin with `w` + emitter.instruction("cmp rax, 3"); // tag 3 = boolean scalar? + emitter.instruction("je __rt_proc_open_scalar_mode_read_win"); // boolean text cannot begin with `w` + emitter.instruction("cmp rax, 8"); // tag 8 = null scalar? + emitter.instruction("jne __rt_proc_open_cleanup_m1_win"); // non-scalar mode remains unsupported by this runtime + emitter.label("__rt_proc_open_scalar_mode_read_win"); + emitter.instruction("mov eax, 1"); // PHP scalar-to-string coercion produces a non-write-leading mode + emitter.instruction("jmp __rt_proc_open_mode_direction_ready_win"); // release owned boxes using the common direction path + + // -- match php-src: only a leading "w" changes the pipe direction -- + emitter.label("__rt_proc_open_string_mode_win"); + emitter.instruction("test rdx, rdx"); // mode string is empty? + emitter.instruction("jz __rt_proc_open_empty_mode_read_win"); // empty modes use php-src's non-write/read direction + emitter.instruction("cmp BYTE PTR [rdi], 0x77"); // first byte is 'w'? + emitter.instruction("setne al"); // is_read = first byte is not 'w' + emitter.instruction("movzx rax, al"); // widen the direction flag for its stack slot + emitter.instruction("jmp __rt_proc_open_mode_direction_ready_win"); // retain the computed non-empty mode direction + emitter.label("__rt_proc_open_empty_mode_read_win"); + emitter.instruction("mov eax, 1"); // empty modes also make the child read + emitter.label("__rt_proc_open_mode_direction_ready_win"); + emitter.instruction("mov QWORD PTR [rbp - 80], rax"); // save is_read in the sub_ptr slot (no longer needed) + + // -- release the three owned boxes now that is_read is known -- + emitter.instruction("mov rax, QWORD PTR [rbp - 88]"); // m0 + abi::emit_call_label(emitter, "__rt_decref_mixed"); // drop the caller's ref on m0 + emitter.instruction("mov rax, QWORD PTR [rbp - 96]"); // m1 + abi::emit_call_label(emitter, "__rt_decref_mixed"); // drop the caller's ref on m1 + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // sub_box + abi::emit_call_label(emitter, "__rt_decref_mixed"); // drop the caller's ref on sub_box + + // -- open a pipe pair: CreatePipe(&read_end, &write_end, &sa, 0) -- + emitter.instruction("lea rcx, [rbp - 104]"); // &read_end (out param) + emitter.instruction("lea rdx, [rbp - 112]"); // &write_end (out param) + emitter.instruction("lea r8, [rbp - 384]"); // &sa (heritable security attributes) + emitter.instruction("xor r9d, r9d"); // nSize = 0 (default pipe buffer size) + emitter.instruction("call CreatePipe"); // BOOL in eax; fills read_end/write_end + emitter.instruction("test eax, eax"); // CreatePipe failed? + emitter.instruction("jz __rt_proc_open_capture_cleanup_win"); // capture errno, then cleanup opened pipes + fail + + // -- record pipe ends: only a write-leading mode makes the child write -- + emitter.instruction("mov r10, QWORD PTR [rbp - 104]"); // read_end + emitter.instruction("mov r11, QWORD PTR [rbp - 112]"); // write_end + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // i (descriptor index) + emitter.instruction("mov r8, QWORD PTR [rbp - 80]"); // reload is_read + emitter.instruction("test r8, r8"); // a write-leading mode gives the child write_end + emitter.instruction("jz __rt_proc_open_write_mode_win"); // is_read == 0 -> write mode + emitter.instruction("lea rax, [rbp - 288]"); // base of child_handle + emitter.instruction("mov QWORD PTR [rax + r9 * 8], r10"); // non-write-leading mode gives the child read_end + emitter.instruction("lea rax, [rbp - 224]"); // base of parent_handle + emitter.instruction("mov QWORD PTR [rax + r9 * 8], r11"); // non-write-leading mode gives the parent write_end + emitter.instruction("jmp __rt_proc_open_pipe_recorded_win"); // skip the write-mode assignment + emitter.label("__rt_proc_open_write_mode_win"); + emitter.instruction("lea rax, [rbp - 288]"); // base of child_handle + emitter.instruction("mov QWORD PTR [rax + r9 * 8], r11"); // write-leading mode gives the child write_end + emitter.instruction("lea rax, [rbp - 224]"); // base of parent_handle + emitter.instruction("mov QWORD PTR [rax + r9 * 8], r10"); // write-leading mode gives the parent read_end + emitter.label("__rt_proc_open_pipe_recorded_win"); + + // -- make the parent end non-inheritable (only the child end must be inherited) -- + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // reload i + emitter.instruction("lea rax, [rbp - 224]"); // base of parent_handle + emitter.instruction("mov rcx, QWORD PTR [rax + r9 * 8]"); // parent_handle[i] + emitter.instruction("mov rdx, 1"); // HANDLE_FLAG_INHERIT + emitter.instruction("xor r8d, r8d"); // dwFlags = 0 (clear inherit -> non-inheritable) + emitter.instruction("call SetHandleInformation"); // the parent keeps a non-inheritable end + emitter.instruction("test eax, eax"); // did clearing HANDLE_FLAG_INHERIT succeed? + emitter.instruction("jnz __rt_proc_open_parent_inherit_ok_win"); // yes -> continue wiring the child handle + emitter.instruction("inc QWORD PTR [rbp - 64]"); // include this newly opened pair in the cleanup bound + emitter.instruction("jmp __rt_proc_open_capture_cleanup_win"); // preserve GetLastError before closing handles + emitter.label("__rt_proc_open_parent_inherit_ok_win"); + + // -- mark the pipe before sharing standard-handle wiring with other descriptors -- + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // reload i + emitter.instruction("mov r10, 1"); // bit 0 = pipe owns a parent endpoint + emitter.instruction("cmp QWORD PTR [rbp - 80], 0"); // did the child receive the write end (parent reads)? + emitter.instruction("jne __rt_proc_open_pipe_state_ready_win"); // child-read mode leaves the parent write-only + emitter.instruction("or r10, 2"); // bit 1 = parent owns the readable end + emitter.label("__rt_proc_open_pipe_state_ready_win"); + emitter.instruction("lea rax, [rbp - 352]"); // base of descriptor state + emitter.instruction("mov QWORD PTR [rax + r9 * 8], r10"); // record pipe ownership and parent direction + emitter.instruction("jmp __rt_proc_open_std_wire_win"); // wire the child handle into STARTUPINFOW + + // -- wire the child end into STARTUPINFOW for stdin/stdout/stderr (key < 3 only) -- + emitter.label("__rt_proc_open_std_wire_win"); + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // reload bookkeeping position i + emitter.instruction("lea rax, [rbp - 288]"); // base of child_handle + emitter.instruction("mov r10, QWORD PTR [rax + r9 * 8]"); // child_handle[i] + emitter.instruction("lea rax, [rbp - 784]"); // base of real descriptor-key bookkeeping + emitter.instruction("mov r9, QWORD PTR [rax + r9 * 8]"); // reload the actual child descriptor key + emitter.instruction("cmp r9, 0"); // fd 0 (stdin)? + emitter.instruction("jne __rt_proc_open_std_check1_win"); // skip the stdin slot when the descriptor index is not zero + emitter.instruction("mov QWORD PTR [rbp - 408], r10"); // si.hStdInput = child_handle[0] + emitter.instruction("jmp __rt_proc_open_std_done_win"); // finish after assigning the child stdin handle + emitter.label("__rt_proc_open_std_check1_win"); + emitter.instruction("cmp r9, 1"); // fd 1 (stdout)? + emitter.instruction("jne __rt_proc_open_std_check2_win"); // skip the stdout slot when the descriptor index is not one + emitter.instruction("mov QWORD PTR [rbp - 400], r10"); // si.hStdOutput = child_handle[1] + emitter.instruction("jmp __rt_proc_open_std_done_win"); // finish after assigning the child stdout handle + emitter.label("__rt_proc_open_std_check2_win"); + emitter.instruction("cmp r9, 2"); // fd 2 (stderr)? + emitter.instruction("jne __rt_proc_open_std_done_win"); // fd >= 3: no standard slot (documented C1c limitation) + emitter.instruction("mov QWORD PTR [rbp - 392], r10"); // si.hStdError = child_handle[2] + emitter.label("__rt_proc_open_std_done_win"); + + // -- advance the loop index -- + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // reload i + emitter.instruction("inc r9"); // i += 1 + emitter.instruction("mov QWORD PTR [rbp - 64], r9"); // persist the loop index + emitter.instruction("jmp __rt_proc_open_loop_test_win"); // continue the descriptor loop + + // -- non-pipe descriptors: dispatch PHP's null/redirect/file forms -- + emitter.label("__rt_proc_open_nonpipe_descriptor_win"); + emitter.instruction("mov rax, QWORD PTR [rbp - 88]"); // reload the descriptor-type mixed box + abi::emit_call_label(emitter, "__rt_mixed_unbox"); // rdi/rsi = descriptor type bytes and length + emitter.instruction("cmp rax, 1"); // descriptor type remained a string? + emitter.instruction("jne __rt_proc_open_cleanup_m0_win"); // malformed type cannot name a descriptor source + emitter.instruction("cmp rdx, 4"); // null/file are both four-byte names + emitter.instruction("jne __rt_proc_open_nonpipe_nonshort_win"); // socket/redirect use longer names + emitter.instruction("cmp DWORD PTR [rdi], 0x6c6c756e"); // "null" in little-endian byte order? + emitter.instruction("je __rt_proc_open_null_descriptor_win"); // create an inheritable NUL device handle + emitter.instruction("cmp DWORD PTR [rdi], 0x656c6966"); // "file" in little-endian byte order? + emitter.instruction("je __rt_proc_open_file_descriptor_win"); // open a strict UTF-8 path for the child + emitter.instruction("jmp __rt_proc_open_cleanup_m0_win"); // unsupported descriptor string + emitter.label("__rt_proc_open_nonpipe_nonshort_win"); + emitter.instruction("cmp rdx, 6"); // socket has six bytes + emitter.instruction("jne __rt_proc_open_nonpipe_long_win"); // redirect is the remaining supported name + emitter.instruction("cmp DWORD PTR [rdi], 0x6b636f73"); // first half of "socket" + emitter.instruction("jne __rt_proc_open_cleanup_m0_win"); // unsupported six-byte descriptor + emitter.instruction("cmp WORD PTR [rdi + 4], 0x7465"); // second half of "socket" + emitter.instruction("jne __rt_proc_open_cleanup_m0_win"); // unsupported six-byte descriptor + emitter.instruction("jmp __rt_proc_open_socket_descriptor_win"); // construct private loopback stream pair + emitter.label("__rt_proc_open_nonpipe_long_win"); + emitter.instruction("cmp rdx, 8"); // redirect has eight bytes + emitter.instruction("jne __rt_proc_open_cleanup_m0_win"); // unsupported descriptor string + emitter.instruction("cmp DWORD PTR [rdi], 0x69646572"); // first half of "redirect" + emitter.instruction("jne __rt_proc_open_cleanup_m0_win"); // not a redirect descriptor + emitter.instruction("cmp DWORD PTR [rdi + 4], 0x74636572"); // second half of "redirect" + emitter.instruction("jne __rt_proc_open_cleanup_m0_win"); // not a redirect descriptor + emitter.instruction("jmp __rt_proc_open_redirect_descriptor_win"); // duplicate an earlier child descriptor + + // -- descriptor `null`: CreateFileW(NUL) with inheritable attributes -- + emitter.label("__rt_proc_open_null_descriptor_win"); + emitter.instruction("mov rax, QWORD PTR [rbp - 88]"); // descriptor type box is no longer needed + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the type box + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // descriptor array box is no longer needed + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the descriptor array box + emitter.instruction("movabs rax, 0x0000004c0055004e"); // materialize UTF-16 "NUL\\0" + emitter.instruction("mov QWORD PTR [rbp - 520], rax"); // retain the device path through CreateFileW + emitter.instruction("lea rcx, [rbp - 520]"); // lpFileName = NUL device + emitter.instruction("mov rdx, 0xC0000000"); // allow child reads and writes against NUL + emitter.instruction("mov r8, 3"); // share reads and writes like php-src streams + emitter.instruction("lea r9, [rbp - 384]"); // inheritable SECURITY_ATTRIBUTES + emitter.instruction("mov QWORD PTR [rsp + 32], 3"); // OPEN_EXISTING for the NUL device + emitter.instruction("mov QWORD PTR [rsp + 40], 0x80"); // FILE_ATTRIBUTE_NORMAL + emitter.instruction("mov QWORD PTR [rsp + 48], 0"); // no template handle + emitter.instruction("call CreateFileW"); // open the inherited NUL endpoint + emitter.instruction("cmp rax, -1"); // INVALID_HANDLE_VALUE? + emitter.instruction("je __rt_proc_open_capture_cleanup_win"); // preserve native failure before cleanup + emitter.instruction("mov QWORD PTR [rbp - 656], rax"); // retain the child-only NUL handle + emitter.instruction("jmp __rt_proc_open_child_handle_ready_win"); // record and wire the child handle + + // -- descriptor `socket`: private loopback pair, never the public AF_UNIX API -- + emitter.label("__rt_proc_open_socket_descriptor_win"); + emitter.instruction("mov rax, QWORD PTR [rbp - 88]"); // descriptor type box is no longer needed + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the socket name box + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // descriptor array box is no longer needed + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release descriptor array box + emitter.instruction("lea rdi, [rbp - 104]"); // parent raw SOCKET scratch output + emitter.instruction("lea rsi, [rbp - 112]"); // child raw SOCKET scratch output + emitter.instruction("call __rt_proc_open_socketpair_win"); // build a connected AF_INET loopback pair + emitter.instruction("test rax, rax"); // private pair construction succeeded? + emitter.instruction("jnz __rt_proc_open_cleanup_win"); // helper already published Winsock errno + emitter.instruction("mov rcx, -1"); // source process = current process pseudo-handle + emitter.instruction("mov rdx, QWORD PTR [rbp - 104]"); // inherited parent SOCKET from the private pair + emitter.instruction("mov r8, -1"); // target process = current process pseudo-handle + emitter.instruction("lea r9, [rbp - 656]"); // receive the non-inheritable parent duplicate + emitter.instruction("mov QWORD PTR [rsp + 32], 0"); // desired access ignored with DUPLICATE_SAME_ACCESS + emitter.instruction("mov QWORD PTR [rsp + 40], 0"); // parent duplicate must not be inherited + emitter.instruction("mov QWORD PTR [rsp + 48], 3"); // DUPLICATE_CLOSE_SOURCE | DUPLICATE_SAME_ACCESS + emitter.instruction("call DuplicateHandle"); // match php-src's make_descriptor_cloexec(parent) + emitter.instruction("test eax, eax"); // did parent cloexec duplication succeed? + emitter.instruction("jz __rt_proc_open_socket_parent_duplicate_failed_win"); // source closed even on failure; release child separately + emitter.instruction("mov rax, QWORD PTR [rbp - 656]"); // non-inheritable parent SOCKET duplicate + emitter.instruction("mov QWORD PTR [rbp - 104], rax"); // replace the closed original parent endpoint + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // descriptor position + emitter.instruction("mov r11, QWORD PTR [rbp - 104]"); // parent raw SOCKET + emitter.instruction("lea r10, [rbp - 224]"); // parent SOCKET table + emitter.instruction("mov QWORD PTR [r10 + r9 * 8], r11"); // store parent SOCKET at descriptor position + emitter.instruction("mov r11, QWORD PTR [rbp - 112]"); // child raw SOCKET + emitter.instruction("lea r10, [rbp - 288]"); // child SOCKET table + emitter.instruction("mov QWORD PTR [r10 + r9 * 8], r11"); // store child SOCKET at descriptor position + emitter.instruction("lea r10, [rbp - 352]"); // descriptor state table + emitter.instruction("mov QWORD PTR [r10 + r9 * 8], 8"); // bit 3 = raw SOCKET parent/child pair + emitter.instruction("jmp __rt_proc_open_std_wire_win"); // assign key 0/1/2 into STARTUPINFOW + emitter.label("__rt_proc_open_socket_parent_duplicate_failed_win"); + emitter.instruction("call GetLastError"); // preserve DuplicateHandle failure before closing the child endpoint + emitter.instruction("mov QWORD PTR [rbp - 624], rax"); // source parent is already closed by DUPLICATE_CLOSE_SOURCE + emitter.instruction("mov rcx, QWORD PTR [rbp - 112]"); // child endpoint was not registered in the descriptor tables + emitter.instruction("call closesocket"); // release the remaining socket without generic double-close + emitter.instruction("jmp __rt_proc_open_create_failure_mode_restored_win"); // translate the saved native error and release prior descriptors + + // -- descriptor resource: duplicate its CRT stream handle for the child -- + emitter.label("__rt_proc_open_resource_descriptor_win"); + emitter.instruction("cmp rdx, 1"); // kind 1 = native stream resource? + emitter.instruction("jne __rt_proc_open_cleanup_sub_win"); // process/other resources are not descriptor streams + emitter.instruction("mov QWORD PTR [rbp - 632], rdi"); // retain the stream fd across decref + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // descriptor resource box + abi::emit_call_label(emitter, "__rt_decref_mixed"); // caller keeps the original stream resource alive + emitter.instruction("mov ecx, DWORD PTR [rbp - 632]"); // CRT fd passed with the MSx64 ABI + emitter.instruction("call _get_osfhandle"); // recover the owned source HANDLE without consuming it + emitter.instruction("cmp rax, -1"); // invalid CRT descriptor? + emitter.instruction("je __rt_proc_open_resource_invalid_fd_win"); // report deterministic EBADF instead of stale GetLastError + emitter.instruction("mov QWORD PTR [rbp - 104], rax"); // source HANDLE for DuplicateHandle + emitter.instruction("jmp __rt_proc_open_duplicate_child_handle_win"); // create an inheritable child-owned copy + + // -- descriptor redirect: resolve an earlier real descriptor key -- + emitter.label("__rt_proc_open_redirect_descriptor_win"); + emitter.instruction("mov rax, QWORD PTR [rbp - 88]"); // descriptor type box + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the already-matched redirect name + emitter.instruction("mov rdi, QWORD PTR [rbp - 80]"); // descriptor sub-array + emitter.instruction("mov esi, 1"); // redirect target lives at sub[1] + emitter.instruction("mov rdx, -1"); // integer-key sentinel + emitter.instruction("xor ecx, ecx"); // suppress a missing-target warning + abi::emit_call_label(emitter, "__rt_array_get_mixed_key"); // own the redirect target box + emitter.instruction("mov QWORD PTR [rbp - 96], rax"); // retain the target box across unbox + abi::emit_call_label(emitter, "__rt_mixed_unbox"); // rdi = target descriptor number + emitter.instruction("cmp rax, 0"); // redirect target must be an integer + emitter.instruction("jne __rt_proc_open_redirect_release_fail_win"); // malformed target -> release boxes and fail + emitter.instruction("mov QWORD PTR [rbp - 632], rdi"); // retain target key while releasing boxes + emitter.instruction("mov rax, QWORD PTR [rbp - 96]"); // redirect target box + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the target box + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // descriptor array box + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the descriptor array box + emitter.instruction("mov QWORD PTR [rbp - 640], 0"); // search position = 0 + emitter.label("__rt_proc_open_redirect_find_win"); + emitter.instruction("mov r9, QWORD PTR [rbp - 640]"); // candidate prior descriptor position + emitter.instruction("cmp r9, QWORD PTR [rbp - 64]"); // only descriptors already processed are legal targets + emitter.instruction("jae __rt_proc_open_redirect_std_fallback_win"); // otherwise PHP permits std handles 0/1/2 + emitter.instruction("lea r10, [rbp - 784]"); // base of actual descriptor keys + emitter.instruction("mov r11, QWORD PTR [r10 + r9 * 8]"); // candidate key + emitter.instruction("cmp r11, QWORD PTR [rbp - 632]"); // target key matches this prior entry? + emitter.instruction("je __rt_proc_open_redirect_found_win"); // duplicate the matching child handle + emitter.instruction("inc r9"); // inspect the next prior descriptor + emitter.instruction("mov QWORD PTR [rbp - 640], r9"); // persist the search cursor + emitter.instruction("jmp __rt_proc_open_redirect_find_win"); // continue lookup + emitter.label("__rt_proc_open_redirect_found_win"); + emitter.instruction("lea r10, [rbp - 288]"); // base of earlier child handles + emitter.instruction("mov rax, QWORD PTR [r10 + r9 * 8]"); // source HANDLE to duplicate + emitter.instruction("mov QWORD PTR [rbp - 104], rax"); // preserve source across DuplicateHandle setup + emitter.instruction("jmp __rt_proc_open_duplicate_child_handle_win"); // inherit an independent child copy + emitter.label("__rt_proc_open_redirect_std_fallback_win"); + emitter.instruction("cmp QWORD PTR [rbp - 632], 2"); // fallback is defined only for standard descriptors 0/1/2 + emitter.instruction("ja __rt_proc_open_cleanup_win"); // unknown/forward descriptor target fails cleanly + emitter.instruction("mov ecx, -10"); // STD_INPUT_HANDLE for descriptor zero + emitter.instruction("sub ecx, DWORD PTR [rbp - 632]"); // 1/2 map to STD_OUTPUT_HANDLE/STD_ERROR_HANDLE + emitter.instruction("call GetStdHandle"); // recover the inherited parent standard HANDLE + emitter.instruction("test rax, rax"); // absent standard handle? + emitter.instruction("jz __rt_proc_open_capture_cleanup_win"); // map the Win32 failure before cleanup + emitter.instruction("cmp rax, -1"); // INVALID_HANDLE_VALUE? + emitter.instruction("je __rt_proc_open_capture_cleanup_win"); // map the Win32 failure before cleanup + emitter.instruction("mov QWORD PTR [rbp - 104], rax"); // source HANDLE remains owned by the process + emitter.instruction("jmp __rt_proc_open_duplicate_child_handle_win"); // duplicate an inheritable child copy + + // -- descriptor file: parse PHP fopen-style mode and open a strict UTF-8 path -- + emitter.label("__rt_proc_open_file_descriptor_win"); + emitter.instruction("mov rax, QWORD PTR [rbp - 88]"); // descriptor type box is no longer needed + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the matched file name + emitter.instruction("mov QWORD PTR [rbp - 104], 0"); // no mode box exists until sub[2] lookup succeeds + emitter.instruction("mov QWORD PTR [rbp - 704], 0"); // no narrow path staging buffer exists yet + emitter.instruction("mov rdi, QWORD PTR [rbp - 80]"); // descriptor sub-array + emitter.instruction("mov esi, 1"); // file path lives at sub[1] + emitter.instruction("mov rdx, -1"); // integer-key sentinel + emitter.instruction("xor ecx, ecx"); // suppress missing-path warning + abi::emit_call_label(emitter, "__rt_array_get_mixed_key"); // own the path box + emitter.instruction("mov QWORD PTR [rbp - 96], rax"); // retain path box across mode lookup + abi::emit_call_label(emitter, "__rt_mixed_unbox"); // rdi/rdx = UTF-8 path bytes/length + emitter.instruction("cmp rax, 1"); // path must be a string + emitter.instruction("jne __rt_proc_open_file_release_fail_win"); // reject a non-string path + emitter.instruction("mov QWORD PTR [rbp - 632], rdi"); // save path bytes through mode parsing + emitter.instruction("mov QWORD PTR [rbp - 640], rdx"); // save path length through mode parsing + emitter.instruction("mov rdi, QWORD PTR [rbp - 80]"); // descriptor sub-array + emitter.instruction("mov esi, 2"); // fopen mode lives at sub[2] + emitter.instruction("mov rdx, -1"); // integer-key sentinel + emitter.instruction("xor ecx, ecx"); // suppress missing-mode warning + abi::emit_call_label(emitter, "__rt_array_get_mixed_key"); // own the mode box + emitter.instruction("mov QWORD PTR [rbp - 104], rax"); // retain mode box across unbox + abi::emit_call_label(emitter, "__rt_mixed_unbox"); // rdi/rdx = mode bytes/length + emitter.instruction("cmp rax, 1"); // mode must be a string + emitter.instruction("jne __rt_proc_open_file_release_fail_win"); // reject a non-string mode + emitter.instruction("test rdx, rdx"); // an empty fopen mode is invalid + emitter.instruction("jz __rt_proc_open_file_release_fail_win"); // fail before opening a file + emitter.instruction("mov QWORD PTR [rbp - 656], rdi"); // retain mode bytes for plus scanning + emitter.instruction("mov QWORD PTR [rbp - 664], rdx"); // retain mode length for plus scanning + emitter.instruction("mov rax, 0x80000000"); // materialize GENERIC_READ without an invalid memory immediate + emitter.instruction("mov QWORD PTR [rbp - 672], rax"); // default `r` access = GENERIC_READ + emitter.instruction("mov QWORD PTR [rbp - 680], 3"); // default `r` disposition = OPEN_EXISTING + emitter.instruction("mov QWORD PTR [rbp - 688], 0"); // append seek is disabled by default + emitter.instruction("cmp BYTE PTR [rdi], 0x72"); // mode starts with `r`? + emitter.instruction("je __rt_proc_open_file_mode_ready_win"); // keep read/open-existing defaults + emitter.instruction("mov rax, 0x40000000"); // materialize GENERIC_WRITE without an invalid memory immediate + emitter.instruction("mov QWORD PTR [rbp - 672], rax"); // non-read modes start with GENERIC_WRITE + emitter.instruction("cmp BYTE PTR [rdi], 0x77"); // mode starts with `w`? + emitter.instruction("je __rt_proc_open_file_mode_w_win"); // CREATE_ALWAYS + emitter.instruction("cmp BYTE PTR [rdi], 0x61"); // mode starts with `a`? + emitter.instruction("je __rt_proc_open_file_mode_a_win"); // OPEN_ALWAYS then seek end + emitter.instruction("cmp BYTE PTR [rdi], 0x78"); // mode starts with `x`? + emitter.instruction("je __rt_proc_open_file_mode_x_win"); // CREATE_NEW + emitter.instruction("cmp BYTE PTR [rdi], 0x63"); // mode starts with `c`? + emitter.instruction("jne __rt_proc_open_file_release_fail_win"); // unsupported PHP fopen mode + emitter.instruction("mov QWORD PTR [rbp - 680], 4"); // `c` = OPEN_ALWAYS without truncation + emitter.instruction("jmp __rt_proc_open_file_mode_ready_win"); // scan optional plus marker + emitter.label("__rt_proc_open_file_mode_w_win"); + emitter.instruction("mov QWORD PTR [rbp - 680], 2"); // `w` = CREATE_ALWAYS + emitter.instruction("jmp __rt_proc_open_file_mode_ready_win"); // scan optional plus marker + emitter.label("__rt_proc_open_file_mode_a_win"); + emitter.instruction("mov QWORD PTR [rbp - 680], 4"); // `a` = OPEN_ALWAYS + emitter.instruction("mov QWORD PTR [rbp - 688], 1"); // seek to end before child execution + emitter.instruction("jmp __rt_proc_open_file_mode_ready_win"); // scan optional plus marker + emitter.label("__rt_proc_open_file_mode_x_win"); + emitter.instruction("mov QWORD PTR [rbp - 680], 1"); // `x` = CREATE_NEW + emitter.label("__rt_proc_open_file_mode_ready_win"); + emitter.instruction("mov QWORD PTR [rbp - 696], 0"); // mode scan index = 0 + emitter.label("__rt_proc_open_file_plus_scan_win"); + emitter.instruction("mov r9, QWORD PTR [rbp - 696]"); // reload mode scan index + emitter.instruction("cmp r9, QWORD PTR [rbp - 664]"); // consumed every mode byte? + emitter.instruction("jae __rt_proc_open_file_plus_done_win"); // no plus marker present + emitter.instruction("mov r10, QWORD PTR [rbp - 656]"); // mode byte pointer + emitter.instruction("cmp BYTE PTR [r10 + r9], 0x2b"); // this byte is `+`? + emitter.instruction("je __rt_proc_open_file_plus_seen_win"); // enable read/write access + emitter.instruction("inc r9"); // advance to the next mode byte + emitter.instruction("mov QWORD PTR [rbp - 696], r9"); // persist mode scan index + emitter.instruction("jmp __rt_proc_open_file_plus_scan_win"); // continue scanning `b`/`t`/`+` suffixes + emitter.label("__rt_proc_open_file_plus_seen_win"); + emitter.instruction("mov rax, 0xC0000000"); // materialize GENERIC_READ | GENERIC_WRITE + emitter.instruction("mov QWORD PTR [rbp - 672], rax"); // `+` requests GENERIC_READ | GENERIC_WRITE + emitter.label("__rt_proc_open_file_plus_done_win"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 632]"); // counted UTF-8 path bytes + emitter.instruction("mov rcx, QWORD PTR [rbp - 640]"); // counted UTF-8 path length + emitter.instruction("xor eax, eax"); // scan byte = NUL + emitter.instruction("repne scasb"); // reject a path that CreateFileW would silently truncate + emitter.instruction("je __rt_proc_open_file_release_eilseq_win"); // embedded NUL is not a representable Windows path + emitter.instruction("mov rax, QWORD PTR [rbp - 640]"); // counted UTF-8 path length + emitter.instruction("inc rax"); // reserve the required NUL terminator + emitter.instruction("jz __rt_proc_open_file_release_nomem_win"); // allocation-size overflow is ENOMEM + emitter.instruction("mov QWORD PTR [rbp - 712], rax"); // retain narrow path allocation size + emitter.instruction("call __rt_heap_alloc"); // allocate writable NUL-terminated path staging + emitter.instruction("mov QWORD PTR [rbp - 704], rax"); // retain narrow staging through conversion + emitter.instruction("test rax, rax"); // path staging allocation succeeded? + emitter.instruction("jz __rt_proc_open_file_release_nomem_win"); // release boxes and report ENOMEM + emitter.instruction("mov rdi, rax"); // destination narrow path cursor + emitter.instruction("mov rsi, QWORD PTR [rbp - 632]"); // source counted UTF-8 path bytes + emitter.instruction("mov rcx, QWORD PTR [rbp - 640]"); // copy the complete path byte sequence + emitter.instruction("cld"); // copy forward into the owned staging buffer + emitter.instruction("rep movsb"); // materialize the counted path without spare-byte assumptions + emitter.instruction("mov BYTE PTR [rdi], 0"); // append the NUL required by the shared conversion helper + emitter.instruction("mov rdi, QWORD PTR [rbp - 704]"); // NUL-terminated UTF-8 path staging + emitter.instruction("call __rt_win_utf8_to_utf16"); // strict path conversion for CreateFileW + emitter.instruction("mov QWORD PTR [rbp - 648], rax"); // retain owned wide path through CreateFileW + emitter.instruction("test rax, rax"); // strict conversion or allocation succeeded? + emitter.instruction("jz __rt_proc_open_file_release_eilseq_win"); // path cannot be represented as UTF-16 + emitter.instruction("mov rax, QWORD PTR [rbp - 704]"); // owned NUL-terminated path staging + emitter.instruction("call __rt_heap_free"); // release narrow staging after strict conversion + emitter.instruction("mov QWORD PTR [rbp - 704], 0"); // prevent failure cleanup from freeing it twice + emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // mode mixed box + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release mode after parsing it + emitter.instruction("mov rax, QWORD PTR [rbp - 96]"); // path mixed box + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release path after conversion + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // descriptor array box + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release descriptor array + emitter.instruction("mov rcx, QWORD PTR [rbp - 648]"); // strict UTF-16 file path + emitter.instruction("mov rdx, QWORD PTR [rbp - 672]"); // parsed desired access + emitter.instruction("mov r8, 3"); // FILE_SHARE_READ | FILE_SHARE_WRITE + emitter.instruction("lea r9, [rbp - 384]"); // inherited child handle attributes + emitter.instruction("mov rax, QWORD PTR [rbp - 680]"); // parsed creation disposition + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // dwCreationDisposition + emitter.instruction("mov QWORD PTR [rsp + 40], 0x80"); // FILE_ATTRIBUTE_NORMAL + emitter.instruction("mov QWORD PTR [rsp + 48], 0"); // no template handle + emitter.instruction("call CreateFileW"); // open the child-only file handle + emitter.instruction("mov QWORD PTR [rbp - 656], rax"); // preserve CreateFileW result through path cleanup + emitter.instruction("cmp rax, -1"); // file open succeeded? + emitter.instruction("je __rt_proc_open_file_create_failed_win"); // capture error before releasing the wide path + emitter.instruction("mov rax, QWORD PTR [rbp - 648]"); // owned strict UTF-16 path + emitter.instruction("call __rt_heap_free"); // release the path after successful open + emitter.instruction("cmp QWORD PTR [rbp - 688], 0"); // append mode requested? + emitter.instruction("je __rt_proc_open_child_handle_ready_win"); // no seek needed before standard-handle wiring + emitter.instruction("mov rcx, QWORD PTR [rbp - 656]"); // file handle for SetFilePointerEx + emitter.instruction("xor edx, edx"); // zero distance value + emitter.instruction("xor r8d, r8d"); // caller does not need the resulting position + emitter.instruction("mov r9d, 2"); // FILE_END + emitter.instruction("call SetFilePointerEx"); // place append-mode file at EOF + emitter.instruction("test eax, eax"); // append positioning succeeded? + emitter.instruction("jz __rt_proc_open_file_append_failed_win"); // capture and close this unregistered child handle + emitter.instruction("jmp __rt_proc_open_child_handle_ready_win"); // record the child-only file handle + + // -- duplicate an existing child handle while preserving the source owner -- + emitter.label("__rt_proc_open_duplicate_child_handle_win"); + emitter.instruction("mov rcx, -1"); // source process = current process pseudo-handle + emitter.instruction("mov rdx, QWORD PTR [rbp - 104]"); // source HANDLE (never consumed) + emitter.instruction("mov r8, -1"); // target process = current process pseudo-handle + emitter.instruction("lea r9, [rbp - 656]"); // out: independently owned child HANDLE + emitter.instruction("mov QWORD PTR [rsp + 32], 0"); // desired access ignored with DUPLICATE_SAME_ACCESS + emitter.instruction("mov QWORD PTR [rsp + 40], 1"); // inherited by the child at CreateProcessW + emitter.instruction("mov QWORD PTR [rsp + 48], 2"); // DUPLICATE_SAME_ACCESS + emitter.instruction("call DuplicateHandle"); // create the inheritable child-only copy + emitter.instruction("test eax, eax"); // duplication succeeded? + emitter.instruction("jz __rt_proc_open_capture_cleanup_win"); // preserve native failure before cleanup + emitter.instruction("jmp __rt_proc_open_child_handle_ready_win"); // record and wire the duplicate + + // -- record a child-only descriptor and share standard-handle wiring -- + emitter.label("__rt_proc_open_child_handle_ready_win"); + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // current descriptor position + emitter.instruction("lea r10, [rbp - 288]"); // base of child handle table + emitter.instruction("mov rax, QWORD PTR [rbp - 656]"); // acquired child-only HANDLE + emitter.instruction("mov QWORD PTR [r10 + r9 * 8], rax"); // store child HANDLE for post-spawn close + emitter.instruction("lea r10, [rbp - 352]"); // base of descriptor state table + emitter.instruction("mov QWORD PTR [r10 + r9 * 8], 16"); // bit 4 = child-only non-pipe descriptor + emitter.instruction("jmp __rt_proc_open_std_wire_win"); // apply real descriptor key 0/1/2 to STARTUPINFOW + + // -- malformed redirect target: the type box was already released -- + emitter.label("__rt_proc_open_redirect_release_fail_win"); + emitter.instruction("mov rax, QWORD PTR [rbp - 96]"); // owned malformed target box + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the target box exactly once + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // descriptor array box + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the descriptor array box + emitter.instruction("jmp __rt_proc_open_cleanup_win"); // prior descriptors remain owned by generic cleanup + + // -- file-only early failures release every owned parsed descriptor box -- + emitter.label("__rt_proc_open_file_release_fail_win"); + emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // optional mode box + emitter.instruction("test rax, rax"); // was mode lookup reached? + emitter.instruction("jz __rt_proc_open_file_release_path_win"); // no mode box to release + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the mode box + emitter.label("__rt_proc_open_file_release_path_win"); + emitter.instruction("mov rax, QWORD PTR [rbp - 96]"); // path box + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the path box + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // descriptor array box + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the descriptor array box + emitter.instruction("jmp __rt_proc_open_cleanup_win"); // prior descriptors are still the only owned handles + emitter.label("__rt_proc_open_file_release_eilseq_win"); + emitter.instruction("mov rax, QWORD PTR [rbp - 704]"); // optional narrow staging after failed conversion + emitter.instruction("test rax, rax"); // was path staging allocated? + emitter.instruction("jz __rt_proc_open_file_release_eilseq_boxes_win"); // no staging buffer to release + emitter.instruction("call __rt_heap_free"); // release failed-conversion path staging + emitter.label("__rt_proc_open_file_release_eilseq_boxes_win"); + emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // mode box remains owned after failed conversion + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the mode box + emitter.instruction("mov rax, QWORD PTR [rbp - 96]"); // path box remains owned after failed conversion + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the path box + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // descriptor array box remains owned + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the descriptor array box + emitter.instruction("jmp __rt_proc_open_invalid_utf8_win"); // publish EILSEQ for an invalid file path + emitter.label("__rt_proc_open_file_release_nomem_win"); + emitter.instruction("mov rax, QWORD PTR [rbp - 96]"); // path box remains owned after allocation failure + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the path box + emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // mode box remains owned after allocation failure + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the mode box + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // descriptor array box remains owned + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the descriptor array box + emitter.instruction("jmp __rt_proc_open_alloc_fail_win"); // publish ENOMEM and release prior descriptors + emitter.label("__rt_proc_open_file_create_failed_win"); + emitter.instruction("call GetLastError"); // capture CreateFileW failure before freeing the path + emitter.instruction("mov QWORD PTR [rbp - 624], rax"); // preserve native error code + emitter.instruction("mov rax, QWORD PTR [rbp - 648]"); // owned strict UTF-16 file path + emitter.instruction("call __rt_heap_free"); // release path after failed open + emitter.instruction("mov rax, QWORD PTR [rbp - 624]"); // restore the native error code + emitter.instruction("call __rt_win32_errno_from_code"); // publish portable errno before generic cleanup + emitter.instruction("jmp __rt_proc_open_cleanup_win"); // release prior descriptors only + emitter.label("__rt_proc_open_file_append_failed_win"); + emitter.instruction("call GetLastError"); // capture seek failure before closing the file handle + emitter.instruction("mov QWORD PTR [rbp - 624], rax"); // preserve native error code + emitter.instruction("mov rcx, QWORD PTR [rbp - 656]"); // unregistered child-only file handle + emitter.instruction("call CloseHandle"); // avoid leaking the failed append descriptor + emitter.instruction("mov rax, QWORD PTR [rbp - 624]"); // restore native error code + emitter.instruction("call __rt_win32_errno_from_code"); // publish portable errno before cleanup + emitter.instruction("jmp __rt_proc_open_cleanup_win"); // prior descriptors are still the only owned handles + emitter.label("__rt_proc_open_resource_invalid_fd_win"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 9"); // EBADF: stream resource has no CRT HANDLE + emitter.instruction("jmp __rt_proc_open_cleanup_win"); // previous descriptors remain the only owned handles + + // -- fill any unwired std handle (0/1/2) with a redirect to NUL -- + emitter.label("__rt_proc_open_nul_fill_win"); + emitter.instruction("movabs rax, 0x0000004c0055004e"); // materialize UTF-16 "NUL\0" device path + emitter.instruction("mov QWORD PTR [rbp - 520], rax"); // store the four WCHARs in the frame + + emitter.instruction("cmp QWORD PTR [rbp - 408], 0"); // si.hStdInput already wired? + emitter.instruction("jne __rt_proc_open_nul_stdout_win"); // wired -> skip + emitter.instruction("lea rcx, [rbp - 520]"); // "NUL" + emitter.instruction("mov rdx, 0xC0000000"); // GENERIC_READ | GENERIC_WRITE + emitter.instruction("mov r8, 3"); // FILE_SHARE_READ | FILE_SHARE_WRITE + emitter.instruction("lea r9, [rbp - 384]"); // &sa (heritable) + emitter.instruction("mov QWORD PTR [rsp + 32], 3"); // dwCreationDisposition = OPEN_EXISTING + emitter.instruction("mov QWORD PTR [rsp + 40], 0x80"); // dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL + emitter.instruction("mov QWORD PTR [rsp + 48], 0"); // hTemplateFile = NULL + emitter.instruction("call CreateFileW"); // open Unicode NUL for the missing stdin redirect + emitter.instruction("cmp rax, -1"); // INVALID_HANDLE_VALUE? + emitter.instruction("je __rt_proc_open_capture_cleanup_win"); // capture the native error and release prior handles + emitter.instruction("mov QWORD PTR [rbp - 144], rax"); // nul_handle[0] = NUL handle + emitter.instruction("mov QWORD PTR [rbp - 408], rax"); // si.hStdInput = NUL handle + emitter.label("__rt_proc_open_nul_stdout_win"); + emitter.instruction("cmp QWORD PTR [rbp - 400], 0"); // si.hStdOutput already wired? + emitter.instruction("jne __rt_proc_open_nul_stderr_win"); // wired -> skip + emitter.instruction("lea rcx, [rbp - 520]"); // "NUL" + emitter.instruction("mov rdx, 0xC0000000"); // GENERIC_READ | GENERIC_WRITE + emitter.instruction("mov r8, 3"); // FILE_SHARE_READ | FILE_SHARE_WRITE + emitter.instruction("lea r9, [rbp - 384]"); // &sa (heritable) + emitter.instruction("mov QWORD PTR [rsp + 32], 3"); // dwCreationDisposition = OPEN_EXISTING + emitter.instruction("mov QWORD PTR [rsp + 40], 0x80"); // dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL + emitter.instruction("mov QWORD PTR [rsp + 48], 0"); // hTemplateFile = NULL + emitter.instruction("call CreateFileW"); // open Unicode NUL for the missing stdout redirect + emitter.instruction("cmp rax, -1"); // INVALID_HANDLE_VALUE? + emitter.instruction("je __rt_proc_open_capture_cleanup_win"); // capture the native error and release prior handles + emitter.instruction("mov QWORD PTR [rbp - 152], rax"); // nul_handle[1] = NUL handle + emitter.instruction("mov QWORD PTR [rbp - 400], rax"); // si.hStdOutput = NUL handle + emitter.label("__rt_proc_open_nul_stderr_win"); + emitter.instruction("cmp QWORD PTR [rbp - 392], 0"); // si.hStdError already wired? + emitter.instruction("jne __rt_proc_open_cmdline_win"); // wired -> skip + emitter.instruction("lea rcx, [rbp - 520]"); // "NUL" + emitter.instruction("mov rdx, 0xC0000000"); // GENERIC_READ | GENERIC_WRITE + emitter.instruction("mov r8, 3"); // FILE_SHARE_READ | FILE_SHARE_WRITE + emitter.instruction("lea r9, [rbp - 384]"); // &sa (heritable) + emitter.instruction("mov QWORD PTR [rsp + 32], 3"); // dwCreationDisposition = OPEN_EXISTING + emitter.instruction("mov QWORD PTR [rsp + 40], 0x80"); // dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL + emitter.instruction("mov QWORD PTR [rsp + 48], 0"); // hTemplateFile = NULL + emitter.instruction("call CreateFileW"); // open Unicode NUL for the missing stderr redirect + emitter.instruction("cmp rax, -1"); // INVALID_HANDLE_VALUE? + emitter.instruction("je __rt_proc_open_capture_cleanup_win"); // capture the native error and release prior handles + emitter.instruction("mov QWORD PTR [rbp - 160], rax"); // nul_handle[2] = NUL handle + emitter.instruction("mov QWORD PTR [rbp - 392], rax"); // si.hStdError = NUL handle + + // -- build a direct command line or a cmd.exe shell staging buffer -- + emitter.label("__rt_proc_open_cmdline_win"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // scan the counted PHP string for embedded NUL bytes + emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // command byte length + emitter.instruction("xor eax, eax"); // search byte = NUL + emitter.instruction("repne scasb"); // Windows command lines cannot represent embedded NUL + emitter.instruction("je __rt_proc_open_invalid_utf8_win"); // reject truncating an embedded-NUL PHP string + emitter.instruction("mov r9, QWORD PTR [rbp - 568]"); // load packed environment/direct flags + emitter.instruction("test r9, 1"); // direct array command or bypass_shell? + emitter.instruction("jnz __rt_proc_open_cmd_size_win"); // direct mode has no shell wrapper + emitter.instruction("mov QWORD PTR [rbp - 584], 15"); // shell prefix byte count + emitter.instruction("mov QWORD PTR [rbp - 592], 1"); // shell closing-quote byte count + emitter.label("__rt_proc_open_cmd_size_win"); + emitter.instruction("mov r9, QWORD PTR [rbp - 32]"); // start with command bytes + emitter.instruction("add r9, QWORD PTR [rbp - 584]"); // include the optional shell prefix + emitter.instruction("add r9, QWORD PTR [rbp - 592]"); // include the optional closing quote + emitter.instruction("inc r9"); // include the NUL terminator + emitter.instruction("mov QWORD PTR [rbp - 56], r9"); // stash total size across the heap calls + emitter.instruction("call GetProcessHeap"); // rax = default process heap + emitter.instruction("mov rcx, rax"); // heap handle (arg1) + emitter.instruction("xor edx, edx"); // dwFlags = 0 (arg2) + emitter.instruction("mov r8, QWORD PTR [rbp - 56]"); // dwBytes (arg3), reloaded after GetProcessHeap + emitter.instruction("call HeapAlloc"); // rax = cmdbuf pointer + emitter.instruction("mov QWORD PTR [rbp - 136], rax"); // save cmdbuf: CreateProcessW input + cleanup key + emitter.instruction("mov QWORD PTR [rbp - 56], 0"); // total-size scratch is now the wide_cmdbuf slot + emitter.instruction("test rax, rax"); // did the narrow staging allocation succeed? + emitter.instruction("jz __rt_proc_open_alloc_fail_win"); // no -> publish ENOMEM and clean up pipes + + // -- write the optional literal "cmd.exe /s /c \"" prefix -- + emitter.instruction("mov rdi, QWORD PTR [rbp - 136]"); // cmdbuf pointer + emitter.instruction("cmp QWORD PTR [rbp - 584], 0"); // direct mode has no prefix + emitter.instruction("je __rt_proc_open_copy_command_win"); // skip the shell literal in direct mode + emitter.instruction("mov BYTE PTR [rdi + 0], 0x63"); // 'c' + emitter.instruction("mov BYTE PTR [rdi + 1], 0x6d"); // 'm' + emitter.instruction("mov BYTE PTR [rdi + 2], 0x64"); // 'd' + emitter.instruction("mov BYTE PTR [rdi + 3], 0x2e"); // '.' + emitter.instruction("mov BYTE PTR [rdi + 4], 0x65"); // 'e' + emitter.instruction("mov BYTE PTR [rdi + 5], 0x78"); // 'x' + emitter.instruction("mov BYTE PTR [rdi + 6], 0x65"); // 'e' + emitter.instruction("mov BYTE PTR [rdi + 7], 0x20"); // ' ' + emitter.instruction("mov BYTE PTR [rdi + 8], 0x2f"); // '/' + emitter.instruction("mov BYTE PTR [rdi + 9], 0x73"); // 's' + emitter.instruction("mov BYTE PTR [rdi + 10], 0x20"); // ' ' + emitter.instruction("mov BYTE PTR [rdi + 11], 0x2f"); // '/' + emitter.instruction("mov BYTE PTR [rdi + 12], 0x63"); // 'c' + emitter.instruction("mov BYTE PTR [rdi + 13], 0x20"); // ' ' + emitter.instruction("mov BYTE PTR [rdi + 14], 0x22"); // '"' (opening quote around the command) + + // -- copy the raw command bytes after the selected prefix -- + emitter.label("__rt_proc_open_copy_command_win"); + emitter.instruction("cld"); // ensure forward direction for the string copy + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // command pointer + emitter.instruction("add rdi, QWORD PTR [rbp - 584]"); // advance past the selected prefix + emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // command length + emitter.instruction("rep movsb"); // copy the command bytes; rdi now points past them + + // -- append the optional shell quote and mandatory NUL terminator -- + emitter.instruction("cmp QWORD PTR [rbp - 592], 0"); // shell mode needs a closing quote + emitter.instruction("je __rt_proc_open_terminate_command_win"); // direct mode terminates immediately + emitter.instruction("mov BYTE PTR [rdi], 0x22"); // append the shell command's closing quote + emitter.instruction("inc rdi"); // advance past the closing quote + emitter.label("__rt_proc_open_terminate_command_win"); + emitter.instruction("mov BYTE PTR [rdi], 0"); // NUL terminator + + // -- strictly convert the complete writable command line to UTF-16 -- + emitter.instruction("mov rdi, QWORD PTR [rbp - 136]"); // NUL-terminated UTF-8 command line + emitter.instruction("call __rt_win_utf8_to_utf16"); // strict MB_ERR_INVALID_CHARS conversion to owned WCHAR buffer + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // preserve wide_cmdbuf for spawn and cleanup + emitter.instruction("test rax, rax"); // invalid UTF-8 or allocation failure? + emitter.instruction("jz __rt_proc_open_invalid_utf8_win"); // fail without passing a truncated/invalid command to Windows + + // -- materialize an optional counted cwd string as strict UTF-16 -- + emitter.instruction("cmp QWORD PTR [rbp - 528], 0"); // was a non-null cwd supplied? + emitter.instruction("je __rt_proc_open_cwd_ready_win"); // null -> inherit the parent current directory + emitter.instruction("mov rdi, QWORD PTR [rbp - 528]"); // scan the counted cwd for embedded NUL bytes + emitter.instruction("mov rcx, QWORD PTR [rbp - 536]"); // cwd byte length + emitter.instruction("xor eax, eax"); // search byte = NUL + emitter.instruction("repne scasb"); // Win32 current-directory strings cannot contain embedded NUL + emitter.instruction("je __rt_proc_open_invalid_utf8_win"); // reject a cwd that would be truncated + emitter.instruction("mov r9, QWORD PTR [rbp - 536]"); // cwd byte length + emitter.instruction("inc r9"); // include the trailing NUL in the staging allocation + emitter.instruction("mov QWORD PTR [rbp - 544], r9"); // temporarily preserve allocation size + emitter.instruction("call GetProcessHeap"); // rax = default process heap + emitter.instruction("mov rcx, rax"); // heap handle + emitter.instruction("xor edx, edx"); // allocation flags = 0 + emitter.instruction("mov r8, QWORD PTR [rbp - 544]"); // cwd staging allocation size + emitter.instruction("call HeapAlloc"); // allocate writable NUL-terminated cwd staging + emitter.instruction("mov QWORD PTR [rbp - 544], rax"); // save cwd narrow staging pointer + emitter.instruction("test rax, rax"); // allocation succeeded? + emitter.instruction("jz __rt_proc_open_alloc_fail_win"); // no -> ENOMEM + cleanup + emitter.instruction("mov rdi, rax"); // destination cwd staging cursor + emitter.instruction("mov rsi, QWORD PTR [rbp - 528]"); // source counted cwd bytes + emitter.instruction("mov rcx, QWORD PTR [rbp - 536]"); // source byte length + emitter.instruction("cld"); // copy forward + emitter.instruction("rep movsb"); // copy the complete cwd + emitter.instruction("mov BYTE PTR [rdi], 0"); // append NUL terminator + emitter.instruction("mov rdi, QWORD PTR [rbp - 544]"); // NUL-terminated UTF-8 cwd + emitter.instruction("call __rt_win_utf8_to_utf16"); // strict cwd conversion + emitter.instruction("mov QWORD PTR [rbp - 552], rax"); // preserve owned UTF-16 cwd + emitter.instruction("test rax, rax"); // conversion succeeded? + emitter.instruction("jz __rt_proc_open_invalid_utf8_win"); // invalid UTF-8 -> EILSEQ + cleanup + emitter.label("__rt_proc_open_cwd_ready_win"); + + // -- strictly convert the counted double-NUL UTF-8 environment block -- + emitter.instruction("cmp QWORD PTR [rbp - 560], 0"); // was a custom environment supplied? + emitter.instruction("je __rt_proc_open_environment_ready_win"); // null means inherit the parent environment + emitter.instruction("mov rax, QWORD PTR [rbp - 568]"); // packed byte length/direct flag + emitter.instruction("shr rax, 5"); // recover the complete UTF-8 block byte length + emitter.instruction("test rax, rax"); // the block must include its final double NUL + emitter.instruction("jz __rt_proc_open_invalid_utf8_win"); // reject an inconsistent internal descriptor + emitter.instruction("mov QWORD PTR [rbp - 600], rax"); // preserve source byte length across API calls + emitter.instruction("mov ecx, 65001"); // CodePage = CP_UTF8 + emitter.instruction("mov edx, 8"); // dwFlags = MB_ERR_INVALID_CHARS + emitter.instruction("mov r8, QWORD PTR [rbp - 560]"); // source counted UTF-8 block + emitter.instruction("mov r9d, eax"); // source byte count, including embedded NULs + emitter.instruction("mov QWORD PTR [rsp + 32], 0"); // sizing pass has no destination buffer + emitter.instruction("mov QWORD PTR [rsp + 40], 0"); // sizing pass destination capacity = 0 + emitter.instruction("call MultiByteToWideChar"); // eax = required UTF-16 code units + emitter.instruction("test eax, eax"); // strict sizing conversion succeeded? + emitter.instruction("jz __rt_proc_open_invalid_utf8_win"); // malformed UTF-8 cannot reach CreateProcessW + emitter.instruction("mov QWORD PTR [rbp - 608], rax"); // preserve required UTF-16 code-unit count + emitter.instruction("mov rax, QWORD PTR [rbp - 608]"); // required UTF-16 code units + emitter.instruction("shl rax, 1"); // convert code units to allocation bytes + emitter.instruction("call __rt_heap_alloc"); // allocate the owned UTF-16 environment block + emitter.instruction("mov QWORD PTR [rbp - 576], rax"); // retain the block for spawn and cleanup + emitter.instruction("test rax, rax"); // allocation succeeded? + emitter.instruction("jz __rt_proc_open_alloc_fail_win"); // publish ENOMEM and release prior resources + emitter.instruction("mov ecx, 65001"); // CodePage = CP_UTF8 + emitter.instruction("mov edx, 8"); // dwFlags = MB_ERR_INVALID_CHARS + emitter.instruction("mov r8, QWORD PTR [rbp - 560]"); // source double-NUL UTF-8 block + emitter.instruction("mov r9, QWORD PTR [rbp - 600]"); // complete source byte count + emitter.instruction("mov rax, QWORD PTR [rbp - 576]"); // destination UTF-16 block + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // lpWideCharStr + emitter.instruction("mov rax, QWORD PTR [rbp - 608]"); // destination capacity in code units + emitter.instruction("mov QWORD PTR [rsp + 40], rax"); // cchWideChar + emitter.instruction("call MultiByteToWideChar"); // preserve embedded separators and final double NUL + emitter.instruction("test eax, eax"); // strict conversion succeeded? + emitter.instruction("jz __rt_proc_open_invalid_utf8_win"); // fail before process creation on malformed input + emitter.label("__rt_proc_open_environment_ready_win"); + + // -- Enable php-src's optional error suppression only around CreateProcessW -- + emitter.instruction("mov rax, QWORD PTR [rbp - 568]"); // packed Windows proc_open options + emitter.instruction("test rax, 2"); // suppress_errors enabled? + emitter.instruction("jz __rt_proc_open_error_mode_ready_win"); // default behavior preserves the current process mode + emitter.instruction("mov rcx, 3"); // SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX + emitter.instruction("call SetErrorMode"); // suppress modal Windows critical-error dialogs + emitter.instruction("mov QWORD PTR [rbp - 616], rax"); // preserve the caller's error mode for restoration + emitter.label("__rt_proc_open_error_mode_ready_win"); + + // -- CreateProcessW with direct/shell command and optional Unicode environment -- + emitter.instruction("xor ecx, ecx"); // lpApplicationName = NULL + emitter.instruction("mov rdx, QWORD PTR [rbp - 56]"); // lpCommandLine = wide_cmdbuf (must be writable) + emitter.instruction("xor r8d, r8d"); // lpProcessAttributes = NULL + emitter.instruction("xor r9d, r9d"); // lpThreadAttributes = NULL + emitter.instruction("mov QWORD PTR [rsp + 32], 1"); // bInheritHandles = TRUE + emitter.instruction("mov rax, 0x20"); // NORMAL_PRIORITY_CLASS matches php-src's default + emitter.instruction("test QWORD PTR [rbp - 568], 8"); // create_process_group requested? + emitter.instruction("jz __rt_proc_open_no_process_group_win"); // leave default group inheritance unchanged + emitter.instruction("or rax, 0x200"); // add CREATE_NEW_PROCESS_GROUP + emitter.label("__rt_proc_open_no_process_group_win"); + emitter.instruction("test QWORD PTR [rbp - 568], 16"); // create_new_console requested? + emitter.instruction("jz __rt_proc_open_no_new_console_win"); // retain the parent's console by default + emitter.instruction("or rax, 0x10"); // add CREATE_NEW_CONSOLE + emitter.label("__rt_proc_open_no_new_console_win"); + emitter.instruction("cmp QWORD PTR [rbp - 576], 0"); // custom environment present? + emitter.instruction("je __rt_proc_open_creation_flags_ready_win"); // inherited environment needs no Unicode flag + emitter.instruction("or rax, 0x400"); // add CREATE_UNICODE_ENVIRONMENT + emitter.label("__rt_proc_open_creation_flags_ready_win"); + emitter.instruction("mov QWORD PTR [rsp + 40], rax"); // dwCreationFlags + emitter.instruction("mov rax, QWORD PTR [rbp - 576]"); // optional UTF-16 environment block + emitter.instruction("mov QWORD PTR [rsp + 48], rax"); // lpEnvironment = custom block or inherit + emitter.instruction("mov rax, QWORD PTR [rbp - 552]"); // optional strict UTF-16 cwd (or NULL) + emitter.instruction("mov QWORD PTR [rsp + 56], rax"); // lpCurrentDirectory = cwd or inherit parent's + emitter.instruction("lea rax, [rbp - 488]"); // &si + emitter.instruction("mov QWORD PTR [rsp + 64], rax"); // lpStartupInfo = &si + emitter.instruction("lea rax, [rbp - 512]"); // &pi + emitter.instruction("mov QWORD PTR [rsp + 72], rax"); // lpProcessInformation = &pi + emitter.instruction("call CreateProcessW"); // spawn the child process + emitter.instruction("test eax, eax"); // CreateProcessW failed? + emitter.instruction("jz __rt_proc_open_create_failed_win"); // preserve spawn errno before restoring the caller mode + emitter.instruction("test QWORD PTR [rbp - 568], 2"); // did this call enable suppress_errors? + emitter.instruction("jz __rt_proc_open_error_mode_restored_win"); // no mode restoration is required + emitter.instruction("mov rcx, QWORD PTR [rbp - 616]"); // saved caller error mode + emitter.instruction("call SetErrorMode"); // restore the caller's process-wide error-mode policy + emitter.label("__rt_proc_open_error_mode_restored_win"); + + // -- success: the thread handle is never used, close it immediately -- + emitter.instruction("mov rcx, QWORD PTR [rbp - 504]"); // pi.hThread + emitter.instruction("call CloseHandle"); // close the unused thread handle + + // -- close every child end: the child inherited its own copies at spawn time -- + emitter.instruction("mov QWORD PTR [rbp - 64], 0"); // j = 0 + emitter.label("__rt_proc_open_close_child_test_win"); + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // reload j + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload n + emitter.instruction("cmp r9, r10"); // j < n? + emitter.instruction("jae __rt_proc_open_close_nul_win"); // all child ends closed -> close NUL handles + emitter.instruction("lea rax, [rbp - 352]"); // base of is_pipe + emitter.instruction("mov r11, QWORD PTR [rax + r9 * 8]"); // is_pipe[j] + emitter.instruction("test r11, r11"); // descriptor owns a child handle? + emitter.instruction("jz __rt_proc_open_close_child_next_win"); // skip descriptors with no child handle + emitter.instruction("lea rax, [rbp - 288]"); // base of child_handle + emitter.instruction("mov rcx, QWORD PTR [rax + r9 * 8]"); // child_handle[j] + emitter.instruction("test r11, 8"); // is the child endpoint a raw Winsock SOCKET? + emitter.instruction("jnz __rt_proc_open_close_child_socket_win"); // SOCKETs are not Win32 kernel HANDLEs for CloseHandle + emitter.instruction("call CloseHandle"); // close the parent's copy of the child end + emitter.instruction("jmp __rt_proc_open_close_child_next_win"); // continue after closing the child handle + emitter.label("__rt_proc_open_close_child_socket_win"); + emitter.instruction("call closesocket"); // release the inherited child SOCKET in the parent + emitter.label("__rt_proc_open_close_child_next_win"); + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // reload j + emitter.instruction("inc r9"); // j += 1 + emitter.instruction("mov QWORD PTR [rbp - 64], r9"); // persist j + emitter.instruction("jmp __rt_proc_open_close_child_test_win"); // continue closing child ends + + // -- close any NUL handles opened for unwired std slots -- + emitter.label("__rt_proc_open_close_nul_win"); + emitter.instruction("mov rcx, QWORD PTR [rbp - 144]"); // nul_handle[0] + emitter.instruction("test rcx, rcx"); // was stdin redirected to NUL? + emitter.instruction("jz __rt_proc_open_close_nul1_win"); // not opened -> skip + emitter.instruction("call CloseHandle"); // close the stdin NUL handle + emitter.label("__rt_proc_open_close_nul1_win"); + emitter.instruction("mov rcx, QWORD PTR [rbp - 152]"); // nul_handle[1] + emitter.instruction("test rcx, rcx"); // was stdout redirected to NUL? + emitter.instruction("jz __rt_proc_open_close_nul2_win"); // not opened -> skip + emitter.instruction("call CloseHandle"); // close the stdout NUL handle + emitter.label("__rt_proc_open_close_nul2_win"); + emitter.instruction("mov rcx, QWORD PTR [rbp - 160]"); // nul_handle[2] + emitter.instruction("test rcx, rcx"); // was stderr redirected to NUL? + emitter.instruction("jz __rt_proc_open_free_cmdbuf_win"); // not opened -> skip + emitter.instruction("call CloseHandle"); // close the stderr NUL handle + + // -- free both command buffers: CreateProcessW has already consumed them -- + emitter.label("__rt_proc_open_free_cmdbuf_win"); + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // owned UTF-16 command buffer + emitter.instruction("call __rt_heap_free"); // release the strict-conversion result + emitter.instruction("call GetProcessHeap"); // rax = default process heap + emitter.instruction("mov rcx, rax"); // heap handle (arg1) + emitter.instruction("xor edx, edx"); // dwFlags = 0 (arg2) + emitter.instruction("mov r8, QWORD PTR [rbp - 136]"); // lpMem = cmdbuf (arg3) + emitter.instruction("call HeapFree"); // release the command-line buffer + emitter.instruction("mov rax, QWORD PTR [rbp - 576]"); // optional owned UTF-16 environment block + emitter.instruction("test rax, rax"); // was a custom environment converted? + emitter.instruction("jz __rt_proc_open_free_cwd_win"); // no -> continue with cwd cleanup + emitter.instruction("call __rt_heap_free"); // release the converted environment block + emitter.label("__rt_proc_open_free_cwd_win"); + emitter.instruction("mov rax, QWORD PTR [rbp - 552]"); // optional owned UTF-16 cwd + emitter.instruction("test rax, rax"); // was cwd converted? + emitter.instruction("jz __rt_proc_open_free_cwd_narrow_win"); // no -> skip the wide free + emitter.instruction("call __rt_heap_free"); // release UTF-16 cwd + emitter.label("__rt_proc_open_free_cwd_narrow_win"); + emitter.instruction("cmp QWORD PTR [rbp - 544], 0"); // was cwd narrow staging allocated? + emitter.instruction("je __rt_proc_open_push_begin_win"); // no -> proceed to publish pipes + emitter.instruction("call GetProcessHeap"); // rax = process heap + emitter.instruction("mov rcx, rax"); // heap handle + emitter.instruction("xor edx, edx"); // free flags = 0 + emitter.instruction("mov r8, QWORD PTR [rbp - 544]"); // cwd narrow staging pointer + emitter.instruction("call HeapFree"); // release cwd staging buffer + + // -- adopt every parent HANDLE as a CRT fd, then publish it at its real key -- + // `_open_osfhandle` owns the HANDLE on success. Never call CloseHandle on an + // adopted entry: kind-1 resource release reaches the Windows close shim, + // which calls `_close` and transfers the final HANDLE release to the CRT. + emitter.label("__rt_proc_open_push_begin_win"); + emitter.instruction("mov QWORD PTR [rbp - 64], 0"); // j = 0 + emitter.label("__rt_proc_open_push_test_win"); + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // reload j + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload n + emitter.instruction("cmp r9, r10"); // j < n? + emitter.instruction("jae __rt_proc_open_done_win"); // all pipes pushed -> return hProcess + emitter.instruction("lea rax, [rbp - 352]"); // base of is_pipe + emitter.instruction("mov r11, QWORD PTR [rax + r9 * 8]"); // is_pipe[j] + emitter.instruction("test r11, 1"); // descriptor owns a parent pipe endpoint? + emitter.instruction("jnz __rt_proc_open_parent_pipe_publish_win"); // CRT-adopt ordinary pipe handles only + emitter.instruction("test r11, 8"); // descriptor owns a raw parent Winsock SOCKET? + emitter.instruction("jz __rt_proc_open_push_next_win"); // child-only descriptors are never published in $pipes + emitter.instruction("lea rax, [rbp - 224]"); // base of parent_handle + emitter.instruction("mov rax, QWORD PTR [rax + r9 * 8]"); // raw parent SOCKET remains outside the CRT table + emitter.instruction("mov QWORD PTR [rbp - 88], rax"); // preserve raw socket for resource boxing + emitter.instruction("jmp __rt_proc_open_parent_fd_box_win"); // publish kind-1 resource without _open_osfhandle + emitter.label("__rt_proc_open_parent_pipe_publish_win"); + emitter.instruction("lea rax, [rbp - 224]"); // base of parent_handle + emitter.instruction("mov rax, QWORD PTR [rax + r9 * 8]"); // raw parent HANDLE awaiting CRT adoption + emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // retain the raw HANDLE across _open_osfhandle + emitter.instruction("mov edx, 0x8000"); // _O_BINARY is mandatory for PHP stream bytes + emitter.instruction("test r11, 2"); // does the parent own the readable pipe end? + emitter.instruction("jnz __rt_proc_open_parent_fd_flags_ready_win"); // readable ends use CRT O_RDONLY (zero) + emitter.instruction("or edx, 1"); // write ends use CRT O_WRONLY + emitter.label("__rt_proc_open_parent_fd_flags_ready_win"); + emitter.instruction("mov rcx, QWORD PTR [rbp - 72]"); // raw HANDLE ownership transfers to the CRT on success + emitter.instruction("call _open_osfhandle"); // convert the parent pipe HANDLE into a CRT descriptor + emitter.instruction("cmp eax, -1"); // did CRT descriptor allocation fail before adopting the HANDLE? + emitter.instruction("je __rt_proc_open_parent_fd_fail_win"); // close raw/adopted peers without touching child handles twice + emitter.instruction("mov QWORD PTR [rbp - 88], rax"); // preserve the CRT fd across optional status-cache insertion + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // reload j after the CRT call + emitter.instruction("lea rax, [rbp - 224]"); // base of parent_handle + emitter.instruction("mov r10, QWORD PTR [rbp - 88]"); // newly adopted CRT descriptor + emitter.instruction("mov QWORD PTR [rax + r9 * 8], r10"); // replace raw HANDLE with its CRT fd + emitter.instruction("lea rax, [rbp - 352]"); // base of pipe_state + emitter.instruction("or QWORD PTR [rax + r9 * 8], 4"); // bit 2 = parent HANDLE ownership moved into CRT fd table + // php-src defaults `blocking_pipes` to false. Cache O_NONBLOCK only for + // parent-readable pipe ends; `__rt_sys_read` turns that flag into a + // PeekNamedPipe availability check before it ever enters ReadFile. + emitter.instruction("test QWORD PTR [rbp - 568], 4"); // explicit blocking_pipes=true? + emitter.instruction("jnz __rt_proc_open_parent_fd_box_win"); // true retains normal blocking CRT stream semantics + emitter.instruction("test QWORD PTR [rax + r9 * 8], 2"); // is this parent endpoint readable? + emitter.instruction("jz __rt_proc_open_parent_fd_box_win"); // write-only endpoint has no ReadFile blocking path + emitter.instruction("mov rdi, QWORD PTR [rbp - 88]"); // cache key = parent CRT fd + emitter.instruction("mov rsi, 0x800"); // O_NONBLOCK visible through F_GETFL and read shim + emitter.instruction("call __rt_win_fd_status_upsert"); // retain nonblocking status until close releases this fd + emitter.label("__rt_proc_open_parent_fd_box_win"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 88]"); // kind-1 resource low payload = CRT fd, never a raw HANDLE + emitter.instruction("mov rax, 9"); // tag 9 = resource + emitter.instruction("mov rsi, 1"); // hi = kind 1 (native stream fd closed by resource release) + abi::emit_call_label(emitter, "__rt_mixed_from_value"); // rax = res_box (owned, refcount 1) + emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // save res_box across the push + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // reload j after the allocating resource-box helper + emitter.instruction("lea r10, [rbp - 784]"); // base of real descriptor-key bookkeeping + emitter.instruction("mov rdi, QWORD PTR [r10 + r9 * 8]"); // actual integer descriptor key for this parent pipe + emitter.instruction("xor esi, esi"); // integer keys have no high payload word + emitter.instruction("xor eax, eax"); // tag 0 = integer key Mixed cell + abi::emit_call_label(emitter, "__rt_mixed_from_value"); // allocate the borrowed key cell used by the generic setter + emitter.instruction("mov QWORD PTR [rbp - 88], rax"); // retain the key cell across the container write + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload the current pipes array/hash pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 88]"); // pass the integer descriptor key cell + emitter.instruction("mov rdx, QWORD PTR [rbp - 72]"); // transfer the resource box into the destination container + abi::emit_call_label(emitter, "__rt_array_set_mixed_key"); // set resource at the real descriptor key, returning promoted/reallocated container + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // persist the potentially promoted/reallocated pipes container + emitter.instruction("mov rax, QWORD PTR [rbp - 88]"); // reload the borrowed key cell after the setter read it + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the caller-owned key box; the setter never owns keys + emitter.label("__rt_proc_open_push_next_win"); + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // reload j + emitter.instruction("inc r9"); // j += 1 + emitter.instruction("mov QWORD PTR [rbp - 64], r9"); // persist j + emitter.instruction("jmp __rt_proc_open_push_test_win"); // continue pushing parent ends + + // -- failed CRT adoption after CreateProcessW: child ends/buffers are already released -- + emitter.label("__rt_proc_open_parent_fd_fail_win"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 24"); // EMFILE: CRT descriptor table could not adopt a parent pipe HANDLE + emitter.instruction("mov QWORD PTR [rbp - 520], 0"); // cleanup index j = 0 + emitter.label("__rt_proc_open_parent_fd_cleanup_test_win"); + emitter.instruction("mov r9, QWORD PTR [rbp - 520]"); // reload cleanup j + emitter.instruction("cmp r9, QWORD PTR [rbp - 48]"); // processed every descriptor? + emitter.instruction("jae __rt_proc_open_parent_fd_cleanup_process_win"); // remaining resource is the unreturned process handle + emitter.instruction("lea rax, [rbp - 352]"); // base of pipe_state + emitter.instruction("mov r11, QWORD PTR [rax + r9 * 8]"); // state for this descriptor + emitter.instruction("test r11, 8"); // did a raw socket parent endpoint survive child-end cleanup? + emitter.instruction("jnz __rt_proc_open_parent_fd_cleanup_socket_win"); // raw Winsock sockets are never CRT-adopted + emitter.instruction("test r11, 1"); // was a pipe opened here? + emitter.instruction("jz __rt_proc_open_parent_fd_cleanup_next_win"); // no ownership to release + emitter.instruction("lea rax, [rbp - 224]"); // base of parent_handle / adopted fd slots + emitter.instruction("mov r10, QWORD PTR [rax + r9 * 8]"); // raw HANDLE or CRT fd, selected by adoption bit + emitter.instruction("test r11, 4"); // did _open_osfhandle take ownership? + emitter.instruction("jz __rt_proc_open_parent_fd_cleanup_raw_win"); // raw HANDLE still belongs to this helper + emitter.instruction("mov rdi, r10"); // adopted CRT descriptor for the runtime close shim + emitter.instruction("call __rt_sys_close"); // clears status cache and lets _close release the HANDLE exactly once + emitter.instruction("jmp __rt_proc_open_parent_fd_cleanup_next_win"); // do not CloseHandle an adopted descriptor + emitter.label("__rt_proc_open_parent_fd_cleanup_raw_win"); + emitter.instruction("mov rcx, r10"); // raw HANDLE that CRT never adopted + emitter.instruction("call CloseHandle"); // release the remaining parent handle directly + emitter.instruction("jmp __rt_proc_open_parent_fd_cleanup_next_win"); // avoid treating a pipe HANDLE as a SOCKET + emitter.label("__rt_proc_open_parent_fd_cleanup_socket_win"); + emitter.instruction("lea rax, [rbp - 224]"); // base of published raw parent SOCKETs + emitter.instruction("mov rcx, QWORD PTR [rax + r9 * 8]"); // raw parent SOCKET whose child peer was already closed + emitter.instruction("call closesocket"); // release the published parent socket during late adoption rollback + emitter.label("__rt_proc_open_parent_fd_cleanup_next_win"); + emitter.instruction("inc QWORD PTR [rbp - 520]"); // advance cleanup index + emitter.instruction("jmp __rt_proc_open_parent_fd_cleanup_test_win"); // close the next parent endpoint + emitter.label("__rt_proc_open_parent_fd_cleanup_process_win"); + emitter.instruction("mov rcx, QWORD PTR [rbp - 512]"); // process HANDLE was never returned to a kind-5 resource + emitter.instruction("test rcx, rcx"); // CreateProcessW initializes it on success, retain defensive guard + emitter.instruction("jz __rt_proc_open_fail_win"); // no process handle needs release + emitter.instruction("call CloseHandle"); // release unreturned process HANDLE without terminating the child + emitter.instruction("jmp __rt_proc_open_fail_win"); // report the failed proc_open call as PHP false + + // -- success: return the child process handle -- + emitter.label("__rt_proc_open_done_win"); + emitter.instruction("mov rax, QWORD PTR [rbp - 512]"); // reload pi.hProcess + emitter.instruction("mov rdx, QWORD PTR [rbp - 40]"); // return the final pipes array/hash pointer beside the process handle + emitter.instruction("add rsp, 896"); // release the frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return hProcess (lowerer boxes it as a kind-5 resource) + + // -- cleanup: release owned boxes for the mid-loop failure paths -- + emitter.label("__rt_proc_open_cleanup_sub_win"); + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // sub_box + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release sub_box before cleanup + emitter.instruction("jmp __rt_proc_open_cleanup_win"); // proceed to close opened handles + emitter.label("__rt_proc_open_cleanup_m0_win"); + emitter.instruction("mov rax, QWORD PTR [rbp - 88]"); // m0 + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release m0 + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // sub_box + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release sub_box + emitter.instruction("jmp __rt_proc_open_cleanup_win"); // proceed to close opened handles + emitter.label("__rt_proc_open_cleanup_m1_win"); + emitter.instruction("mov rax, QWORD PTR [rbp - 96]"); // m1 + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release m1 + emitter.instruction("mov rax, QWORD PTR [rbp - 88]"); // m0 + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release m0 + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // sub_box + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release sub_box + emitter.instruction("jmp __rt_proc_open_cleanup_win"); // proceed to close opened handles + + // -- preserve CreateProcessW failure before restoring suppress_errors mode -- + emitter.label("__rt_proc_open_create_failed_win"); + emitter.instruction("call GetLastError"); // capture the failing process-creation error immediately + emitter.instruction("mov QWORD PTR [rbp - 624], rax"); // preserve the native code across SetErrorMode + emitter.instruction("test QWORD PTR [rbp - 568], 2"); // was suppress_errors enabled for this spawn? + emitter.instruction("jz __rt_proc_open_create_failure_mode_restored_win"); // no process mode needs restoring + emitter.instruction("mov rcx, QWORD PTR [rbp - 616]"); // saved caller error mode + emitter.instruction("call SetErrorMode"); // restore the caller's process-wide error-mode policy + emitter.label("__rt_proc_open_create_failure_mode_restored_win"); + emitter.instruction("mov rax, QWORD PTR [rbp - 624]"); // restore the captured native error code + emitter.instruction("call __rt_win32_errno_from_code"); // publish the matching portable errno + emitter.instruction("jmp __rt_proc_open_cleanup_win"); // release handles and report PHP false + + // -- capture a native failure before cleanup calls can overwrite GetLastError -- + emitter.label("__rt_proc_open_capture_cleanup_win"); + emitter.instruction("call GetLastError"); // fetch the failing CreatePipe/File/Process/handle operation's code + emitter.instruction("call __rt_win32_errno_from_code"); // publish the translated POSIX errno + emitter.instruction("jmp __rt_proc_open_cleanup_win"); // release every resource acquired so far + + // -- reject unrepresentable command strings with EILSEQ -- + emitter.label("__rt_proc_open_invalid_utf8_win"); + emitter.instruction("mov QWORD PTR [rip + __rt_errno], 84"); // EILSEQ: embedded NUL or invalid UTF-8 command + emitter.instruction("jmp __rt_proc_open_cleanup_win"); // release pipes and any allocated command buffer + + // -- report command-buffer allocation failure as ENOMEM -- + emitter.label("__rt_proc_open_alloc_fail_win"); + emitter.instruction("mov QWORD PTR [rip + __rt_errno], 12"); // ENOMEM: command-line staging allocation failed + emitter.instruction("jmp __rt_proc_open_cleanup_win"); // release the already-created pipe handles + + // -- cleanup: close every opened child handle and pipe parent endpoint, then NUL handles + cmdbuf -- + // Reached from a mid-loop failure (bound = i, the count already processed) + // or a CreateProcessW failure (bound = n, since the loop finished first). + // Reuses the nul_path slot [rbp - 520] as the cleanup loop counter: by the + // time cleanup runs, the "NUL" literal is no longer needed either way. + emitter.label("__rt_proc_open_cleanup_win"); + emitter.instruction("mov QWORD PTR [rbp - 520], 0"); // cleanup index j = 0 + emitter.label("__rt_proc_open_cleanup_test_win"); + emitter.instruction("mov r9, QWORD PTR [rbp - 520]"); // reload cleanup j + emitter.instruction("mov r10, QWORD PTR [rbp - 64]"); // bound = i (descriptors processed so far) + emitter.instruction("cmp r9, r10"); // j < bound? + emitter.instruction("jae __rt_proc_open_cleanup_nul_win"); // all opened pipes closed -> close NUL handles + emitter.instruction("lea rax, [rbp - 352]"); // base of is_pipe + emitter.instruction("mov r11, QWORD PTR [rax + r9 * 8]"); // is_pipe[j] + emitter.instruction("test r11, r11"); // descriptor acquired any child handle? + emitter.instruction("jz __rt_proc_open_cleanup_next_win"); // skip descriptors that never acquired ownership + emitter.instruction("test r11, 8"); // does this descriptor own a raw Winsock socket pair? + emitter.instruction("jnz __rt_proc_open_cleanup_socket_win"); // sockets require closesocket rather than CloseHandle + emitter.instruction("test r11, 1"); // pipe owns a parent endpoint too? + emitter.instruction("jz __rt_proc_open_cleanup_child_only_win"); // child-only descriptor has no parent handle to close + emitter.instruction("lea rax, [rbp - 224]"); // base of parent_handle + emitter.instruction("mov rcx, QWORD PTR [rax + r9 * 8]"); // parent_handle[j] + emitter.instruction("call CloseHandle"); // close the parent end + emitter.label("__rt_proc_open_cleanup_child_only_win"); + emitter.instruction("mov r9, QWORD PTR [rbp - 520]"); // reload cleanup j (volatile regs clobbered) + emitter.instruction("lea rax, [rbp - 288]"); // base of child_handle + emitter.instruction("mov rcx, QWORD PTR [rax + r9 * 8]"); // child_handle[j] + emitter.instruction("call CloseHandle"); // close the child end + emitter.instruction("jmp __rt_proc_open_cleanup_next_win"); // continue after releasing ordinary handles + emitter.label("__rt_proc_open_cleanup_socket_win"); + emitter.instruction("lea rax, [rbp - 224]"); // base of raw parent SOCKETs + emitter.instruction("mov rcx, QWORD PTR [rax + r9 * 8]"); // parent SOCKET for this descriptor + emitter.instruction("call closesocket"); // release the parent's raw socket endpoint + emitter.instruction("mov r9, QWORD PTR [rbp - 520]"); // reload cleanup j after Winsock call + emitter.instruction("lea rax, [rbp - 288]"); // base of raw child SOCKETs + emitter.instruction("mov rcx, QWORD PTR [rax + r9 * 8]"); // child SOCKET for this descriptor + emitter.instruction("call closesocket"); // release the child's raw socket endpoint + emitter.label("__rt_proc_open_cleanup_next_win"); + emitter.instruction("mov r9, QWORD PTR [rbp - 520]"); // reload cleanup j + emitter.instruction("inc r9"); // j += 1 + emitter.instruction("mov QWORD PTR [rbp - 520], r9"); // persist cleanup j + emitter.instruction("jmp __rt_proc_open_cleanup_test_win"); // continue cleanup + + emitter.label("__rt_proc_open_cleanup_nul_win"); + emitter.instruction("mov rcx, QWORD PTR [rbp - 144]"); // nul_handle[0] + emitter.instruction("test rcx, rcx"); // was stdin redirected to NUL? + emitter.instruction("jz __rt_proc_open_cleanup_nul1_win"); // not opened -> skip + emitter.instruction("call CloseHandle"); // close the stdin NUL handle + emitter.label("__rt_proc_open_cleanup_nul1_win"); + emitter.instruction("mov rcx, QWORD PTR [rbp - 152]"); // nul_handle[1] + emitter.instruction("test rcx, rcx"); // was stdout redirected to NUL? + emitter.instruction("jz __rt_proc_open_cleanup_nul2_win"); // not opened -> skip + emitter.instruction("call CloseHandle"); // close the stdout NUL handle + emitter.label("__rt_proc_open_cleanup_nul2_win"); + emitter.instruction("mov rcx, QWORD PTR [rbp - 160]"); // nul_handle[2] + emitter.instruction("test rcx, rcx"); // was stderr redirected to NUL? + emitter.instruction("jz __rt_proc_open_cleanup_cmdbuf_win"); // not opened -> skip + emitter.instruction("call CloseHandle"); // close the stderr NUL handle + + emitter.label("__rt_proc_open_cleanup_cmdbuf_win"); + emitter.instruction("mov rax, QWORD PTR [rbp - 576]"); // optional wide environment pointer + emitter.instruction("test rax, rax"); // was the environment converted? + emitter.instruction("jz __rt_proc_open_cleanup_cwd_wide_win"); // no -> continue cwd cleanup + emitter.instruction("call __rt_heap_free"); // release the UTF-16 environment block + emitter.label("__rt_proc_open_cleanup_cwd_wide_win"); + emitter.instruction("mov rax, QWORD PTR [rbp - 552]"); // optional wide cwd pointer + emitter.instruction("test rax, rax"); // was cwd converted? + emitter.instruction("jz __rt_proc_open_cleanup_cwd_narrow_win"); // no -> skip wide cwd free + emitter.instruction("call __rt_heap_free"); // release UTF-16 cwd + emitter.label("__rt_proc_open_cleanup_cwd_narrow_win"); + emitter.instruction("cmp QWORD PTR [rbp - 544], 0"); // was cwd narrow staging allocated? + emitter.instruction("je __rt_proc_open_cleanup_wide_cmdbuf_win"); // no -> continue command cleanup + emitter.instruction("call GetProcessHeap"); // rax = process heap + emitter.instruction("mov rcx, rax"); // heap handle + emitter.instruction("xor edx, edx"); // free flags = 0 + emitter.instruction("mov r8, QWORD PTR [rbp - 544]"); // cwd narrow staging pointer + emitter.instruction("call HeapFree"); // release cwd staging + emitter.label("__rt_proc_open_cleanup_wide_cmdbuf_win"); + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // wide_cmdbuf pointer (0 if conversion never succeeded) + emitter.instruction("test rax, rax"); // was the UTF-16 buffer allocated? + emitter.instruction("jz __rt_proc_open_cleanup_narrow_cmdbuf_win"); // no -> continue with the narrow staging buffer + emitter.instruction("call __rt_heap_free"); // release the strict UTF-16 conversion result + emitter.label("__rt_proc_open_cleanup_narrow_cmdbuf_win"); + emitter.instruction("mov rcx, QWORD PTR [rbp - 136]"); // cmdbuf pointer (0 if never allocated) + emitter.instruction("test rcx, rcx"); // was cmdbuf allocated? + emitter.instruction("jz __rt_proc_open_fail_win"); // not allocated -> nothing to free + emitter.instruction("call GetProcessHeap"); // rax = default process heap + emitter.instruction("mov rcx, rax"); // heap handle (arg1) + emitter.instruction("xor edx, edx"); // dwFlags = 0 (arg2) + emitter.instruction("mov r8, QWORD PTR [rbp - 136]"); // lpMem = cmdbuf (arg3) + emitter.instruction("call HeapFree"); // release the command-line buffer + + // -- failure: return -1 (lowerer boxes as PHP false) -- + emitter.label("__rt_proc_open_fail_win"); + emitter.instruction("mov rax, -1"); // report proc_open failure + emitter.instruction("mov rdx, QWORD PTR [rbp - 40]"); // return the original/current pipes container on failure + emitter.instruction("add rsp, 896"); // release the frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the failure sentinel +} + +/// Emits a private AF_INET loopback socket-pair constructor for Windows +/// `proc_open(["socket"])`. This is intentionally distinct from the public +/// AF_UNIX `socketpair` shim, which remains unsupported on PHP Windows. +/// +/// SysV input: `rdi` = parent SOCKET out pointer, `rsi` = child SOCKET out +/// pointer. Returns `0` or `-1` with the Winsock failure mapped to errno. +fn emit_proc_open_socketpair_win(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: proc_open private loopback socket pair ---"); + emitter.label_global("__rt_proc_open_socketpair_win"); + emitter.instruction("sub rsp, 120"); // shadow space, sockaddr_in, sockets, outputs, and saved WSA error + emitter.instruction("mov QWORD PTR [rsp + 96], rdi"); // retain parent output pointer + emitter.instruction("mov QWORD PTR [rsp + 104], rsi"); // retain child output pointer + emitter.instruction("mov QWORD PTR [rsp + 56], -1"); // listener = INVALID_SOCKET + emitter.instruction("mov QWORD PTR [rsp + 64], -1"); // child client = INVALID_SOCKET + emitter.instruction("mov QWORD PTR [rsp + 72], -1"); // parent accepted = INVALID_SOCKET + emitter.instruction("mov WORD PTR [rsp + 32], 2"); // sockaddr_in.sin_family = AF_INET + emitter.instruction("mov WORD PTR [rsp + 34], 0"); // request an ephemeral loopback port + emitter.instruction("mov DWORD PTR [rsp + 36], 0x0100007f"); // sin_addr = network-order 127.0.0.1 + emitter.instruction("mov QWORD PTR [rsp + 40], 0"); // clear sin_zero padding + emitter.instruction("mov DWORD PTR [rsp + 48], 16"); // sizeof(sockaddr_in) + emitter.instruction("mov ecx, 2"); // AF_INET + emitter.instruction("mov edx, 1"); // SOCK_STREAM + emitter.instruction("mov r8d, 6"); // IPPROTO_TCP + emitter.instruction("call socket"); // create loopback listener + emitter.instruction("cmp rax, -1"); // INVALID_SOCKET? + emitter.instruction("je __rt_proc_open_socketpair_fail_win"); // capture WSA error and clean up + emitter.instruction("mov QWORD PTR [rsp + 56], rax"); // retain listener + emitter.instruction("mov rcx, rax"); // listener SOCKET + emitter.instruction("lea rdx, [rsp + 32]"); // loopback sockaddr_in + emitter.instruction("mov r8d, 16"); // sockaddr length + emitter.instruction("call bind"); // bind ephemeral loopback listener + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je __rt_proc_open_socketpair_fail_win"); // clean listener on bind failure + emitter.instruction("mov rcx, QWORD PTR [rsp + 56]"); // listener SOCKET + emitter.instruction("mov edx, 1"); // backlog one connection + emitter.instruction("call listen"); // begin accepting the private pair + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je __rt_proc_open_socketpair_fail_win"); // clean listener on listen failure + emitter.instruction("mov rcx, QWORD PTR [rsp + 56]"); // listener SOCKET + emitter.instruction("lea rdx, [rsp + 32]"); // receive bound loopback address + emitter.instruction("lea r8, [rsp + 48]"); // receive sockaddr length + emitter.instruction("call getsockname"); // discover ephemeral port + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je __rt_proc_open_socketpair_fail_win"); // clean listener on getsockname failure + emitter.instruction("mov ecx, 2"); // AF_INET + emitter.instruction("mov edx, 1"); // SOCK_STREAM + emitter.instruction("mov r8d, 6"); // IPPROTO_TCP + emitter.instruction("xor r9d, r9d"); // lpProtocolInfo = NULL + emitter.instruction("sub rsp, 48"); // isolated shadow and stack arguments preserve the outer sockaddr + emitter.instruction("mov QWORD PTR [rsp + 32], 0"); // g = 0 + emitter.instruction("mov QWORD PTR [rsp + 40], 0"); // dwFlags = 0: child never uses overlapped I/O + emitter.instruction("call WSASocketW"); // match php-src's non-overlapped child endpoint + emitter.instruction("add rsp, 48"); // restore the socket-pair frame and bound sockaddr + emitter.instruction("cmp rax, -1"); // INVALID_SOCKET? + emitter.instruction("je __rt_proc_open_socketpair_fail_win"); // clean listener on socket failure + emitter.instruction("mov QWORD PTR [rsp + 64], rax"); // retain child client SOCKET + emitter.instruction("mov rcx, rax"); // child client SOCKET + emitter.instruction("lea rdx, [rsp + 32]"); // listener endpoint + emitter.instruction("mov r8d, 16"); // sockaddr length + emitter.instruction("call connect"); // connect parent endpoint to listener + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je __rt_proc_open_socketpair_fail_win"); // clean client/listener on connect failure + emitter.instruction("mov rcx, QWORD PTR [rsp + 56]"); // listener SOCKET + emitter.instruction("xor edx, edx"); // caller does not need peer address + emitter.instruction("xor r8d, r8d"); // caller does not need peer length + emitter.instruction("call accept"); // accept child endpoint + emitter.instruction("cmp rax, -1"); // INVALID_SOCKET? + emitter.instruction("je __rt_proc_open_socketpair_fail_win"); // clean client/listener on accept failure + emitter.instruction("mov QWORD PTR [rsp + 72], rax"); // retain parent accepted SOCKET + emitter.instruction("mov rcx, QWORD PTR [rsp + 56]"); // listener is no longer required + emitter.instruction("call closesocket"); // release listener before exposing endpoints + emitter.instruction("mov rax, QWORD PTR [rsp + 72]"); // parent accepted SOCKET + emitter.instruction("mov r9, QWORD PTR [rsp + 96]"); // parent output pointer + emitter.instruction("mov QWORD PTR [r9], rax"); // publish parent raw SOCKET + emitter.instruction("mov rax, QWORD PTR [rsp + 64]"); // child non-overlapped SOCKET + emitter.instruction("mov r9, QWORD PTR [rsp + 104]"); // child output pointer + emitter.instruction("mov QWORD PTR [r9], rax"); // publish child raw SOCKET + emitter.instruction("xor eax, eax"); // pair construction succeeded + emitter.instruction("add rsp, 120"); // restore stack + emitter.instruction("ret"); // return success + emitter.label("__rt_proc_open_socketpair_fail_win"); + emitter.instruction("call WSAGetLastError"); // capture Winsock error before close calls + emitter.instruction("mov DWORD PTR [rsp + 88], eax"); // preserve WSA error code + for (offset, suffix) in [(72, "accepted"), (64, "client"), (56, "listener")] { + emitter.instruction(&format!("mov rcx, QWORD PTR [rsp + {offset}]")); // candidate SOCKET for cleanup + emitter.instruction("cmp rcx, -1"); // endpoint was allocated? + emitter.instruction(&format!("je __rt_proc_open_socketpair_skip_{suffix}_win")); // skip invalid endpoint + emitter.instruction("call closesocket"); // release allocated SOCKET + emitter.label(&format!("__rt_proc_open_socketpair_skip_{suffix}_win")); + } + emitter.instruction("mov eax, DWORD PTR [rsp + 88]"); // restore captured WSA error + emitter.instruction("call __rt_win32_errno_from_code"); // map Winsock error to POSIX errno + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish mapped errno + emitter.instruction("mov rax, -1"); // report pair construction failure + emitter.instruction("add rsp, 120"); // restore stack + emitter.instruction("ret"); // return failure +} + +#[cfg(test)] +mod tests { + //! Purpose: + //! Structural tests for the Windows `proc_open` process-spawn emitter. + //! + //! Called from: + //! - `cargo test` through Rust's test harness. + //! + //! Key details: + //! - These assertions cover Unicode boundaries, handle cleanup, and errno + //! capture that compile-only PE tests cannot observe at runtime. + + use super::*; + use crate::codegen_support::platform::Target; + + /// Verifies Windows spawning uses strict UTF-16 conversion and only Wide + /// Win32 entry points for the command line, file descriptors, and synthetic + /// NUL handles. + #[test] + fn windows_proc_open_uses_strict_wide_process_apis() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_proc_open_win32_x86_64(&mut emitter); + let asm = emitter.output(); + assert_eq!(asm.matches("call __rt_win_utf8_to_utf16").count(), 3); + assert!(asm.contains("call CreateProcessW")); + assert!(asm.contains("call CreateFileW")); + assert!(!asm.contains("CreateProcessA")); + assert!(!asm.contains("CreateFileA")); + assert!(asm.contains("repne scasb"), "embedded NUL must be rejected"); + assert!(asm.contains("mov QWORD PTR [rsp + 56], rax")); + assert!(asm.contains("[rip + __rt_errno], 84"), "invalid UTF-8 must publish EILSEQ"); + } + + /// Verifies native failures preserve errno before cleanup and every owned + /// process/pipe/command-line resource has an explicit release path. + #[test] + fn windows_proc_open_checks_inheritance_and_cleans_every_resource() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_proc_open_win32_x86_64(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("call SetHandleInformation")); + assert!(asm.contains("test eax, eax")); + assert!(asm.contains("__rt_proc_open_capture_cleanup_win:")); + assert!(asm.contains("call GetLastError")); + assert!(asm.contains("call __rt_win32_errno_from_code")); + assert!(asm.contains("call __rt_heap_free"), "wide command buffer must be freed"); + assert!(asm.contains("call HeapFree"), "narrow staging buffer must be freed"); + assert!(asm.matches("call CloseHandle").count() >= 8); + assert!(asm.contains("__rt_proc_open_scalar_mode_read_win:")); + assert!(asm.contains("__rt_proc_open_string_mode_win:")); + } + + /// Verifies every native proc_open emitter accepts PHP-string-coercible + /// scalar pipe modes through its shared non-write-leading direction path. + #[test] + fn proc_open_scalar_pipe_modes_follow_php_read_direction_on_every_target() { + for (target, scalar_label, string_label) in [ + ( + Target::new(Platform::MacOS, Arch::AArch64), + "__rt_proc_open_scalar_mode_read:", + "__rt_proc_open_string_mode:", + ), + ( + Target::new(Platform::Linux, Arch::X86_64), + "__rt_proc_open_scalar_mode_read_x86:", + "__rt_proc_open_string_mode_x86:", + ), + ( + Target::new(Platform::Windows, Arch::X86_64), + "__rt_proc_open_scalar_mode_read_win:", + "__rt_proc_open_string_mode_win:", + ), + ] { + let mut emitter = Emitter::new(target); + emit_proc_open(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains(scalar_label), "missing scalar pipe-mode path for {target:?}"); + assert!(asm.contains(string_label), "missing string pipe-mode path for {target:?}"); + } + } + + /// Verifies direct execution and custom environments use the packed runtime + /// flags, counted strict conversion, and CreateProcessW Unicode contract. + #[test] + fn windows_proc_open_marshals_direct_commands_and_environment() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_proc_open_win32_x86_64(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("test r9, 1")); + assert!(asm.contains("__rt_proc_open_copy_command_win:")); + assert!(asm.contains("shr rax, 5")); + assert!(asm.matches("call MultiByteToWideChar").count() >= 2); + assert!(asm.contains("or rax, 0x400")); + assert!(asm.contains("or rax, 0x200")); + assert!(asm.contains("or rax, 0x10")); + assert!(asm.contains("__rt_proc_open_create_failure_mode_restored_win")); + assert!(asm.contains("mov QWORD PTR [rsp + 48], rax")); + assert!(asm.matches("mov rax, QWORD PTR [rbp - 576]").count() >= 3); + } + + /// Verifies parent pipe handles become CRT descriptors before the kind-1 + /// resources are published, and that default pipe reads acquire the + /// cached O_NONBLOCK state consumed by the Win32 ReadFile shim. + #[test] + fn windows_proc_open_adopts_parent_handles_and_sets_default_nonblocking_reads() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_proc_open_win32_x86_64(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("call _open_osfhandle")); + assert!(asm.contains("cmp eax, -1")); + assert!(asm.contains("mov edx, 0x8000")); + assert!(asm.contains("or edx, 1")); + assert!(asm.contains("or QWORD PTR [rax + r9 * 8], 4")); + assert!(asm.contains("test QWORD PTR [rbp - 568], 4")); + assert!(asm.contains("mov rsi, 0x800")); + assert!(asm.contains("call __rt_win_fd_status_upsert")); + assert!(asm.contains("call __rt_sys_close")); + assert!(asm.contains("__rt_proc_open_parent_fd_cleanup_raw_win:")); + } + + /// Verifies sparse associative descriptor specs retain integer keys through + /// process setup, publish resources with the keyed setter, and return the + /// promoted `$pipes` container in the x86_64 secondary result register. + #[test] + fn windows_proc_open_preserves_sparse_descriptor_keys_and_pipes_result() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_proc_open_win32_x86_64(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("call __rt_hash_iter_next")); + assert!(asm.contains("__rt_proc_open_indexed_key_win:")); + assert!(asm.contains("lea r11, [rbp - 784]")); + assert!(asm.contains("call __rt_array_set_mixed_key")); + assert!(asm.contains("mov QWORD PTR [rbp - 40], rax")); + assert!(asm.contains("mov rdx, QWORD PTR [rbp - 40]")); + assert!(!asm.contains("call __rt_array_push_refcounted")); + } + + /// Verifies Windows descriptor dispatch owns only child handles for + /// `null`, `redirect`, stream-resource, and file sources. Pipe endpoints + /// remain the sole values published to the caller's `$pipes` array. + #[test] + fn windows_proc_open_supports_child_only_descriptor_sources() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_proc_open_win32_x86_64(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("__rt_proc_open_null_descriptor_win:")); + assert!(asm.contains("__rt_proc_open_redirect_descriptor_win:")); + assert!(asm.contains("__rt_proc_open_resource_descriptor_win:")); + assert!(asm.contains("__rt_proc_open_file_descriptor_win:")); + assert!(asm.contains("call DuplicateHandle")); + assert!(asm.contains("call GetStdHandle")); + assert!(asm.contains("call _get_osfhandle")); + assert!(asm.contains("call SetFilePointerEx")); + assert!(asm.contains("repne scasb")); + assert!(asm.contains("[rip + __rt_errno], 9")); + assert!(asm.contains("mov QWORD PTR [r10 + r9 * 8], 16")); + assert!(asm.contains("test r11, 1")); + assert!(asm.contains("__rt_proc_open_cleanup_child_only_win:")); + } + + /// Verifies Windows `proc_open(["socket"])` owns a private TCP loopback + /// pair, publishes only the raw parent endpoint, and never re-enables the + /// public AF_UNIX socketpair API. + #[test] + fn windows_proc_open_supports_private_socket_descriptors() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_proc_open_win32_x86_64(&mut emitter); + emit_proc_open_socketpair_win(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("__rt_proc_open_socket_descriptor_win:")); + assert!(asm.contains("__rt_proc_open_socketpair_win:")); + assert!(asm.contains("call bind")); + assert!(asm.contains("call listen")); + assert!(asm.contains("call WSASocketW")); + assert!(asm.contains("call connect")); + assert!(asm.contains("call accept")); + assert!(asm.contains("test r11, 8")); + assert!(asm.contains("call closesocket")); + assert!(asm.contains("call __rt_win32_errno_from_code")); + assert!(asm.contains("__rt_proc_open_parent_fd_cleanup_socket_win:")); + let socket_descriptor = asm + .split("__rt_proc_open_socket_descriptor_win:") + .nth(1) + .and_then(|tail| tail.split("__rt_proc_open_resource_descriptor_win:").next()) + .expect("socket descriptor emitter section"); + assert!(!socket_descriptor.contains("call SetHandleInformation")); + assert_eq!(socket_descriptor.matches("call DuplicateHandle").count(), 1); + assert!(socket_descriptor.contains("mov QWORD PTR [rsp + 48], 3")); + assert!(!asm.contains("call __rt_sys_socketpair")); + } +} diff --git a/src/codegen_support/runtime/io/proc_open_marshal.rs b/src/codegen_support/runtime/io/proc_open_marshal.rs new file mode 100644 index 0000000000..aea19f1af3 --- /dev/null +++ b/src/codegen_support/runtime/io/proc_open_marshal.rs @@ -0,0 +1,1254 @@ +//! Purpose: +//! Emits Windows runtime marshalling helpers for computed `proc_open` arguments. +//! +//! Called from: +//! - `crate::codegen_support::runtime::io::emit_proc_open_marshalling()`. +//! +//! Key details: +//! - Array commands follow the `CommandLineToArgvW` backslash/quote contract, +//! plus php-src's `cmd.exe`/batch-file caret escaping for later arguments. +//! - Command-processor detection deliberately uses the bounded argv[0] spelling +//! (basename and ASCII-insensitive `cmd`/`.bat`/`.cmd` checks), rather than +//! php-src's allocation-heavy full/long-path canonicalization. +//! - Returned buffers are raw runtime-heap allocations owned by the caller. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::{Arch, Platform}; + +/// Emits the dynamic Windows `proc_open` argument marshallers when targeting PE. +pub(crate) fn emit_proc_open_marshalling(emitter: &mut Emitter) { + if emitter.target.arch != Arch::X86_64 || emitter.target.platform != Platform::Windows { + return; + } + emit_command_array(emitter); + emit_command_uses_cmd(emitter); + emit_options(emitter); + emit_scalar_string(emitter); + emit_key_equal(emitter); + emit_environment(emitter); +} + +/// Emits `__rt_win_proc_environment`, which turns a runtime associative array +/// into an owned, double-NUL UTF-8 block with Windows case-insensitive last-wins +/// de-duplication. Values use PHP scalar-to-string conversion. +fn emit_environment(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: Windows proc_open environment marshalling ---"); + emitter.label_global("__rt_win_proc_environment"); + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable environment frame + emitter.instruction("sub rsp, 192"); // reserve indexed/hash metadata, iteration, and nested-call state + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the runtime environment array + emitter.instruction("mov QWORD PTR [rbp - 24], 0"); // metadata allocation = null + emitter.instruction("mov QWORD PTR [rbp - 96], 0"); // output block allocation = null + emitter.instruction("test rdi, rdi"); // null means inherit, not a custom block + emitter.instruction("jz __rt_win_proc_environment_invalid"); // caller should not marshal omitted environment + emitter.instruction("mov rax, QWORD PTR [rdi - 8]"); // inspect heap storage kind + emitter.instruction("and eax, 0xff"); // isolate kind byte + emitter.instruction("cmp eax, 2"); // indexed storage represents numeric environment entries + emitter.instruction("jne __rt_win_proc_environment_hash"); // associative hashes preserve explicit string keys + emitter.instruction("mov QWORD PTR [rbp - 136], 2"); // remember indexed storage for the collection loop + emitter.instruction("mov rax, QWORD PTR [rdi + 16]"); // load the indexed element stride + emitter.instruction("mov QWORD PTR [rbp - 144], rax"); // preserve the stride across value conversion calls + emitter.instruction("mov rax, QWORD PTR [rdi]"); // numeric environment entries count by array length + emitter.instruction("jmp __rt_win_proc_environment_count_ready"); // build the required double-NUL block + emitter.label("__rt_win_proc_environment_hash"); + emitter.instruction("cmp eax, 3"); // associative storage kind? + emitter.instruction("jne __rt_win_proc_environment_invalid"); // reject non-array runtime storage + emitter.instruction("mov QWORD PTR [rbp - 136], 3"); // remember associative storage for the collection loop + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // pass hash to count helper + emitter.instruction("call __rt_hash_count"); // number of live insertion-order entries + emitter.label("__rt_win_proc_environment_count_ready"); + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // preserve source entry count + emitter.instruction("test rax, rax"); // any metadata rows needed? + emitter.instruction("jz __rt_win_proc_environment_metadata_ready"); // empty environment skips metadata allocation + emitter.instruction("mov rcx, 40"); // five words per retained entry + emitter.instruction("mul rcx"); // bytes = count * 40, overflow in rdx + emitter.instruction("test rdx, rdx"); // multiplication overflow? + emitter.instruction("jnz __rt_win_proc_environment_nomem"); // report unrepresentable allocation as ENOMEM + emitter.instruction("call __rt_heap_alloc"); // allocate borrowed-key/value metadata + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // retain metadata ownership + emitter.instruction("test rax, rax"); // allocation succeeded? + emitter.instruction("jz __rt_win_proc_environment_nomem"); // publish ENOMEM + emitter.label("__rt_win_proc_environment_metadata_ready"); + emitter.instruction("mov QWORD PTR [rbp - 32], 0"); // hash cursor = fresh walk + emitter.instruction("mov QWORD PTR [rbp - 40], 0"); // retained/deduplicated entry count = 0 + emitter.instruction("mov QWORD PTR [rbp - 48], 0"); // processed source entries = 0 + + emitter.label("__rt_win_proc_environment_collect_loop"); + emitter.instruction("mov rax, QWORD PTR [rbp - 48]"); // processed source entries + emitter.instruction("cmp rax, QWORD PTR [rbp - 16]"); // collected every hash entry? + emitter.instruction("jae __rt_win_proc_environment_size_start"); // yes, compute exact block size + emitter.instruction("cmp QWORD PTR [rbp - 136], 2"); // indexed environment entries have no explicit names + emitter.instruction("je __rt_win_proc_environment_collect_indexed"); // load the next numeric array value directly + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // hash iterator source + emitter.instruction("mov rsi, QWORD PTR [rbp - 32]"); // insertion-order cursor + emitter.instruction("call __rt_hash_iter_next"); // return key plus scalar payload triple + emitter.instruction("cmp rax, -1"); // unexpected early end? + emitter.instruction("je __rt_win_proc_environment_invalid"); // inconsistent hash metadata is invalid + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // persist next cursor + emitter.instruction("mov QWORD PTR [rbp - 56], rdi"); // current key pointer + emitter.instruction("mov QWORD PTR [rbp - 64], rdx"); // current key length / integer sentinel + emitter.instruction("mov QWORD PTR [rbp - 72], rcx"); // current value low word + emitter.instruction("mov QWORD PTR [rbp - 80], r8"); // current value high word + emitter.instruction("mov QWORD PTR [rbp - 88], r9"); // current value runtime tag + emitter.instruction("jmp __rt_win_proc_environment_collect_value"); // normalize/validate this associative value + + emitter.label("__rt_win_proc_environment_collect_indexed"); + emitter.instruction("mov rax, QWORD PTR [rbp - 48]"); // current indexed environment entry offset + emitter.instruction("imul rax, QWORD PTR [rbp - 144]"); // scale by the indexed element stride + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the indexed environment base + emitter.instruction("add rdi, 24"); // skip the indexed-array header + emitter.instruction("add rdi, rax"); // address the current numeric environment value + emitter.instruction("mov QWORD PTR [rbp - 56], 0"); // numeric entries do not have a key pointer + emitter.instruction("mov QWORD PTR [rbp - 64], -1"); // key-length sentinel means emit the raw converted value + emitter.instruction("cmp QWORD PTR [rbp - 144], 16"); // direct string elements use pointer/length pairs + emitter.instruction("je __rt_win_proc_environment_indexed_string"); // retain their direct string payload + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the indexed header for its value-type stamp + emitter.instruction("mov r10, QWORD PTR [r10 - 8]"); // inspect packed indexed-array metadata + emitter.instruction("shr r10, 8"); // move the value-type tag into the low byte + emitter.instruction("and r10, 0x7f"); // isolate the direct element runtime tag + emitter.instruction("mov rax, QWORD PTR [rdi]"); // load the direct scalar or boxed Mixed payload + emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // preserve the low payload word + emitter.instruction("mov QWORD PTR [rbp - 80], 0"); // direct scalar slots have no high payload word + emitter.instruction("mov QWORD PTR [rbp - 88], r10"); // preserve the scalar or boxed-Mixed tag + emitter.instruction("cmp r10, 0"); // direct integer values are valid environment scalars + emitter.instruction("je __rt_win_proc_environment_collect_value"); // normalize it through the shared converter + emitter.instruction("cmp r10, 2"); // direct float values are valid environment scalars + emitter.instruction("je __rt_win_proc_environment_collect_value"); // normalize it through the shared converter + emitter.instruction("cmp r10, 3"); // direct boolean values are valid environment scalars + emitter.instruction("je __rt_win_proc_environment_collect_value"); // normalize it through the shared converter + emitter.instruction("cmp r10, 7"); // boxed Mixed values carry their own scalar tag + emitter.instruction("je __rt_win_proc_environment_collect_value"); // normalize the nested Mixed cell + emitter.instruction("cmp r10, 8"); // direct null values cast to empty strings + emitter.instruction("jne __rt_win_proc_environment_invalid"); // arrays/objects/resources cannot populate an environment block + emitter.instruction("jmp __rt_win_proc_environment_collect_value"); // normalize the null scalar + emitter.label("__rt_win_proc_environment_indexed_string"); + emitter.instruction("mov rax, QWORD PTR [rdi]"); // load the direct string pointer + emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // preserve the string pointer as value low word + emitter.instruction("mov rax, QWORD PTR [rdi + 8]"); // load the direct string byte length + emitter.instruction("mov QWORD PTR [rbp - 80], rax"); // preserve the string length as value high word + emitter.instruction("mov QWORD PTR [rbp - 88], 1"); // runtime tag 1 identifies a string payload + + emitter.label("__rt_win_proc_environment_collect_value"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 88]"); // pass the current value tag to the scalar converter + emitter.instruction("mov rsi, QWORD PTR [rbp - 72]"); // pass the current value low word + emitter.instruction("mov rdx, QWORD PTR [rbp - 80]"); // pass the current value high word + emitter.instruction("call __rt_win_proc_scalar_string"); // identify unsupported and empty converted values before retaining rows + emitter.instruction("cmp rdx, -1"); // unsupported environment value? + emitter.instruction("je __rt_win_proc_environment_fail"); // preserve the converter's EINVAL and clean up + emitter.instruction("test rdx, rdx"); // php-src omits values whose string conversion is empty + emitter.instruction("jz __rt_win_proc_environment_skip"); // do not emit or deduplicate an empty value + emitter.instruction("cmp QWORD PTR [rbp - 64], -1"); // numeric key sentinel? + emitter.instruction("je __rt_win_proc_environment_append"); // raw numeric entries bypass string-key validation and deduplication + emitter.instruction("cmp QWORD PTR [rbp - 64], 0"); // empty string keys also become raw environment entries + emitter.instruction("je __rt_win_proc_environment_raw_key"); // normalize the empty key to the numeric sentinel + emitter.instruction("xor r10d, r10d"); // key validation byte offset = 0 + emitter.label("__rt_win_proc_environment_key_scan"); + emitter.instruction("cmp r10, QWORD PTR [rbp - 64]"); // scanned the complete key? + emitter.instruction("jae __rt_win_proc_environment_dedup_start"); // key is representable + emitter.instruction("mov rdi, QWORD PTR [rbp - 56]"); // reload key base + emitter.instruction("movzx eax, BYTE PTR [rdi + r10]"); // load one key byte + emitter.instruction("test al, al"); // embedded NUL? + emitter.instruction("jz __rt_win_proc_environment_invalid"); // block entries cannot contain NUL + emitter.instruction("inc r10"); // inspect the next key byte + emitter.instruction("jmp __rt_win_proc_environment_key_scan"); // continue validation + + emitter.label("__rt_win_proc_environment_dedup_start"); + emitter.instruction("mov QWORD PTR [rbp - 104], 0"); // previous-entry index = 0 + emitter.label("__rt_win_proc_environment_dedup_loop"); + emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // reload previous index + emitter.instruction("cmp rax, QWORD PTR [rbp - 40]"); // compared all retained entries? + emitter.instruction("jae __rt_win_proc_environment_append"); // no match, append a new row + emitter.instruction("imul rax, 40"); // metadata byte offset + emitter.instruction("add rax, QWORD PTR [rbp - 24]"); // previous row address + emitter.instruction("mov rdi, QWORD PTR [rax]"); // previous key pointer + emitter.instruction("mov rsi, QWORD PTR [rax + 8]"); // previous key length + emitter.instruction("mov rdx, QWORD PTR [rbp - 56]"); // current key pointer + emitter.instruction("mov rcx, QWORD PTR [rbp - 64]"); // current key length + emitter.instruction("call __rt_win_proc_env_key_equal"); // Unicode ordinal case-insensitive comparison + emitter.instruction("cmp rax, -1"); // conversion/allocation failure? + emitter.instruction("je __rt_win_proc_environment_fail"); // preserve helper errno and clean up + emitter.instruction("test rax, rax"); // case-insensitive duplicate? + emitter.instruction("jnz __rt_win_proc_environment_replace"); // last occurrence wins + emitter.instruction("inc QWORD PTR [rbp - 104]"); // advance previous-entry index + emitter.instruction("jmp __rt_win_proc_environment_dedup_loop"); // compare against the next retained key + + emitter.label("__rt_win_proc_environment_raw_key"); + emitter.instruction("mov QWORD PTR [rbp - 64], -1"); // represent an empty string key as php-src's raw entry form + emitter.instruction("jmp __rt_win_proc_environment_append"); // raw entries do not participate in key deduplication + + emitter.label("__rt_win_proc_environment_append"); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // append index = retained count + emitter.instruction("inc QWORD PTR [rbp - 40]"); // retain one additional entry + emitter.instruction("jmp __rt_win_proc_environment_store"); // store current key/value + emitter.label("__rt_win_proc_environment_replace"); + emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // overwrite the case-insensitive match + emitter.label("__rt_win_proc_environment_store"); + emitter.instruction("imul rax, 40"); // metadata byte offset + emitter.instruction("add rax, QWORD PTR [rbp - 24]"); // selected metadata row + emitter.instruction("mov r10, QWORD PTR [rbp - 56]"); // current key pointer + emitter.instruction("mov QWORD PTR [rax], r10"); // store borrowed key pointer + emitter.instruction("mov r10, QWORD PTR [rbp - 64]"); // current key length + emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store key length + emitter.instruction("mov r10, QWORD PTR [rbp - 72]"); // current value low word + emitter.instruction("mov QWORD PTR [rax + 16], r10"); // store value low word + emitter.instruction("mov r10, QWORD PTR [rbp - 80]"); // current value high word + emitter.instruction("mov QWORD PTR [rax + 24], r10"); // store value high word + emitter.instruction("mov r10, QWORD PTR [rbp - 88]"); // current value tag + emitter.instruction("mov QWORD PTR [rax + 32], r10"); // store value runtime tag + emitter.instruction("inc QWORD PTR [rbp - 48]"); // one more source entry processed + emitter.instruction("jmp __rt_win_proc_environment_collect_loop"); // collect the next entry + + emitter.label("__rt_win_proc_environment_skip"); + emitter.instruction("inc QWORD PTR [rbp - 48]"); // consume the omitted empty-value source entry + emitter.instruction("jmp __rt_win_proc_environment_collect_loop"); // continue collecting later environment entries + + emitter.label("__rt_win_proc_environment_size_start"); + emitter.instruction("mov QWORD PTR [rbp - 104], 0"); // sizing row index = 0 + emitter.instruction("mov QWORD PTR [rbp - 112], 1"); // reserve the final extra NUL + emitter.instruction("cmp QWORD PTR [rbp - 40], 0"); // empty custom environment? + emitter.instruction("jne __rt_win_proc_environment_size_loop"); // non-empty rows already contribute their first terminator + emitter.instruction("mov QWORD PTR [rbp - 112], 2"); // Windows requires two NULs for an empty environment block + emitter.label("__rt_win_proc_environment_size_loop"); + emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // reload sizing row + emitter.instruction("cmp rax, QWORD PTR [rbp - 40]"); // sized every retained row? + emitter.instruction("jae __rt_win_proc_environment_allocate"); // allocate the exact block + emitter.instruction("imul rax, 40"); // metadata byte offset + emitter.instruction("add rax, QWORD PTR [rbp - 24]"); // current metadata row + emitter.instruction("mov rdi, QWORD PTR [rax + 32]"); // value tag + emitter.instruction("mov rsi, QWORD PTR [rax + 16]"); // value low word + emitter.instruction("mov rdx, QWORD PTR [rax + 24]"); // value high word + emitter.instruction("mov QWORD PTR [rbp - 120], rax"); // preserve row across scalar conversion + emitter.instruction("call __rt_win_proc_scalar_string"); // PHP scalar-to-string conversion + emitter.instruction("cmp rdx, -1"); // unsupported value? + emitter.instruction("je __rt_win_proc_environment_fail"); // helper published EINVAL + emitter.instruction("mov rax, QWORD PTR [rbp - 120]"); // restore metadata row + emitter.instruction("mov rcx, QWORD PTR [rax + 8]"); // key byte length or raw-entry sentinel + emitter.instruction("cmp rcx, -1"); // raw numeric/empty-key entry? + emitter.instruction("je __rt_win_proc_environment_size_raw"); // raw entries contain only the converted value and terminator + emitter.instruction("add rcx, rdx"); // key + converted value + emitter.instruction("jc __rt_win_proc_environment_nomem"); // reject size overflow + emitter.instruction("add rcx, 2"); // '=' plus per-entry NUL + emitter.instruction("jc __rt_win_proc_environment_nomem"); // reject delimiter overflow + emitter.instruction("add QWORD PTR [rbp - 112], rcx"); // accumulate exact block size + emitter.instruction("jc __rt_win_proc_environment_nomem"); // reject total overflow + emitter.instruction("inc QWORD PTR [rbp - 104]"); // advance sizing row + emitter.instruction("jmp __rt_win_proc_environment_size_loop"); // size next entry + emitter.label("__rt_win_proc_environment_size_raw"); + emitter.instruction("mov rcx, rdx"); // raw entries start with the converted value bytes + emitter.instruction("inc rcx"); // include their per-entry NUL terminator + emitter.instruction("jz __rt_win_proc_environment_nomem"); // reject an overflowed raw-entry byte count + emitter.instruction("add QWORD PTR [rbp - 112], rcx"); // accumulate the raw entry size + emitter.instruction("jc __rt_win_proc_environment_nomem"); // reject total raw-block overflow + emitter.instruction("inc QWORD PTR [rbp - 104]"); // advance sizing row after the raw entry + emitter.instruction("jmp __rt_win_proc_environment_size_loop"); // size the next retained row + + emitter.label("__rt_win_proc_environment_allocate"); + emitter.instruction("mov rax, QWORD PTR [rbp - 112]"); // exact block size including final extra NUL + emitter.instruction("call __rt_heap_alloc"); // allocate owned UTF-8 environment block + emitter.instruction("mov QWORD PTR [rbp - 96], rax"); // retain output ownership + emitter.instruction("test rax, rax"); // allocation succeeded? + emitter.instruction("jz __rt_win_proc_environment_nomem"); // publish ENOMEM + emitter.instruction("mov QWORD PTR [rbp - 128], rax"); // output cursor = block base + emitter.instruction("mov QWORD PTR [rbp - 104], 0"); // writing row index = 0 + emitter.label("__rt_win_proc_environment_write_loop"); + emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // reload writing row + emitter.instruction("cmp rax, QWORD PTR [rbp - 40]"); // wrote every retained entry? + emitter.instruction("jae __rt_win_proc_environment_write_final_nul"); // append the block terminator + emitter.instruction("imul rax, 40"); // metadata byte offset + emitter.instruction("add rax, QWORD PTR [rbp - 24]"); // current row + emitter.instruction("mov QWORD PTR [rbp - 120], rax"); // preserve row + emitter.instruction("mov rdi, QWORD PTR [rbp - 128]"); // destination cursor + emitter.instruction("mov rsi, QWORD PTR [rax]"); // key source + emitter.instruction("mov rcx, QWORD PTR [rax + 8]"); // key byte count + emitter.instruction("cmp rcx, -1"); // raw numeric/empty-key entry? + emitter.instruction("je __rt_win_proc_environment_write_value"); // raw entries have no key or '=' delimiter + emitter.instruction("rep movsb"); // copy environment name + emitter.instruction("mov BYTE PTR [rdi], 0x3d"); // append '=' + emitter.instruction("inc rdi"); // advance past '=' + emitter.label("__rt_win_proc_environment_write_value"); + emitter.instruction("mov QWORD PTR [rbp - 128], rdi"); // preserve cursor across value conversion + emitter.instruction("mov rax, QWORD PTR [rbp - 120]"); // restore row + emitter.instruction("mov rdi, QWORD PTR [rax + 32]"); // value tag + emitter.instruction("mov rsi, QWORD PTR [rax + 16]"); // value low word + emitter.instruction("mov rdx, QWORD PTR [rax + 24]"); // value high word + emitter.instruction("call __rt_win_proc_scalar_string"); // reproduce scalar string for copying + emitter.instruction("mov rdi, QWORD PTR [rbp - 128]"); // restore destination cursor + emitter.instruction("mov rsi, rax"); // converted value source + emitter.instruction("mov rcx, rdx"); // converted value byte count + emitter.instruction("rep movsb"); // copy the scalar text + emitter.instruction("mov BYTE PTR [rdi], 0"); // terminate this environment entry + emitter.instruction("inc rdi"); // advance to the next entry + emitter.instruction("mov QWORD PTR [rbp - 128], rdi"); // persist output cursor + emitter.instruction("inc QWORD PTR [rbp - 104]"); // advance writing row + emitter.instruction("jmp __rt_win_proc_environment_write_loop"); // write the next entry + emitter.label("__rt_win_proc_environment_write_final_nul"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 128]"); // final block cursor + emitter.instruction("mov BYTE PTR [rdi], 0"); // append the extra NUL (empty block becomes one NUL only) + emitter.instruction("cmp QWORD PTR [rbp - 40], 0"); // empty custom environment needs another leading terminator + emitter.instruction("jne __rt_win_proc_environment_free_metadata"); // non-empty blocks are already double-NUL terminated + emitter.instruction("mov BYTE PTR [rdi + 1], 0"); // complete the empty environment's double NUL + emitter.label("__rt_win_proc_environment_free_metadata"); + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // metadata allocation + emitter.instruction("test rax, rax"); // non-empty source allocated metadata + emitter.instruction("jz __rt_win_proc_environment_success"); // empty environment has nothing to release + emitter.instruction("call __rt_heap_free"); // release borrowed metadata rows + emitter.label("__rt_win_proc_environment_success"); + emitter.instruction("mov rax, QWORD PTR [rbp - 96]"); // return owned block pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 112]"); // return counted block length + emitter.instruction("add rsp, 192"); // release environment frame + emitter.instruction("pop rbp"); // restore caller frame pointer + emitter.instruction("ret"); // return environment block pair + + emitter.label("__rt_win_proc_environment_invalid"); + emitter.instruction("mov QWORD PTR [rip + __rt_errno], 22"); // EINVAL: malformed environment runtime shape + emitter.instruction("jmp __rt_win_proc_environment_fail"); // clean up partial allocations + emitter.label("__rt_win_proc_environment_nomem"); + emitter.instruction("mov QWORD PTR [rip + __rt_errno], 12"); // ENOMEM: allocation or size overflow + emitter.label("__rt_win_proc_environment_fail"); + emitter.instruction("mov rax, QWORD PTR [rbp - 96]"); // optional partial output block + emitter.instruction("test rax, rax"); // output allocated? + emitter.instruction("jz __rt_win_proc_environment_fail_metadata"); // skip null output + emitter.instruction("call __rt_heap_free"); // release partial block + emitter.label("__rt_win_proc_environment_fail_metadata"); + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // optional metadata allocation + emitter.instruction("test rax, rax"); // metadata allocated? + emitter.instruction("jz __rt_win_proc_environment_fail_return"); // skip null metadata + emitter.instruction("call __rt_heap_free"); // release metadata + emitter.label("__rt_win_proc_environment_fail_return"); + emitter.instruction("xor eax, eax"); // null output signals failure + emitter.instruction("mov rdx, -1"); // distinguish failure from a valid empty environment + emitter.instruction("add rsp, 192"); // release environment frame + emitter.instruction("pop rbp"); // restore caller frame pointer + emitter.instruction("ret"); // return failure +} + +/// Emits the scalar-to-string conversion used for dynamic environment values. +/// Input is `rdi=tag`, `rsi=value_lo`, `rdx=value_hi`; output is `rax`/`rdx`. +fn emit_scalar_string(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: Windows proc_open scalar environment conversion ---"); + emitter.label_global("__rt_win_proc_scalar_string"); + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable conversion frame + emitter.instruction("cmp rdi, 7"); // boxed Mixed hash payload? + emitter.instruction("jne __rt_win_proc_scalar_tag_ready"); // direct values already expose their runtime tag + emitter.instruction("mov rax, rsi"); // pass the nested Mixed pointer + emitter.instruction("call __rt_mixed_unbox"); // rax=tag, rdi=value_lo, rdx=value_hi + emitter.instruction("mov rsi, rdi"); // normalize the low payload register + emitter.instruction("mov rdi, rax"); // normalize the tag register + emitter.label("__rt_win_proc_scalar_tag_ready"); + emitter.instruction("cmp rdi, 0"); // integer payload? + emitter.instruction("je __rt_win_proc_scalar_int"); // format with PHP integer semantics + emitter.instruction("cmp rdi, 1"); // string payload? + emitter.instruction("je __rt_win_proc_scalar_from_string"); // return the borrowed string pair + emitter.instruction("cmp rdi, 2"); // float payload? + emitter.instruction("je __rt_win_proc_scalar_float"); // format with the shared PHP float converter + emitter.instruction("cmp rdi, 3"); // boolean payload? + emitter.instruction("je __rt_win_proc_scalar_bool"); // true="1", false="" + emitter.instruction("cmp rdi, 8"); // null payload? + emitter.instruction("jne __rt_win_proc_scalar_invalid"); // arrays/objects/resources are not environment scalars + emitter.instruction("xor eax, eax"); // null casts to an empty string pointer + emitter.instruction("xor edx, edx"); // null casts to zero bytes + emitter.instruction("jmp __rt_win_proc_scalar_done"); // return the empty string + emitter.label("__rt_win_proc_scalar_int"); + emitter.instruction("mov rax, rsi"); // integer input for the shared formatter + emitter.instruction("call __rt_itoa"); // format a signed PHP integer + emitter.instruction("jmp __rt_win_proc_scalar_done"); // return the scratch slice + emitter.label("__rt_win_proc_scalar_from_string"); + emitter.instruction("mov rax, rsi"); // return the borrowed string pointer + emitter.instruction("jmp __rt_win_proc_scalar_done"); // length is already in rdx + emitter.label("__rt_win_proc_scalar_float"); + emitter.instruction("movq xmm0, rsi"); // move float bits into the formatter register + emitter.instruction("call __rt_ftoa"); // format with PHP-compatible float semantics + emitter.instruction("jmp __rt_win_proc_scalar_done"); // return the scratch slice + emitter.label("__rt_win_proc_scalar_bool"); + emitter.instruction("test rsi, rsi"); // false casts to an empty string + emitter.instruction("jz __rt_win_proc_scalar_empty"); // avoid a formatter call for false + emitter.instruction("mov rax, 1"); // true casts exactly to integer text "1" + emitter.instruction("call __rt_itoa"); // materialize the one-byte true string + emitter.instruction("jmp __rt_win_proc_scalar_done"); // return "1" + emitter.label("__rt_win_proc_scalar_empty"); + emitter.instruction("xor eax, eax"); // empty scalar string pointer + emitter.instruction("xor edx, edx"); // empty scalar string length + emitter.instruction("jmp __rt_win_proc_scalar_done"); // return empty + emitter.label("__rt_win_proc_scalar_invalid"); + emitter.instruction("mov QWORD PTR [rip + __rt_errno], 22"); // EINVAL: environment value is not scalar + emitter.instruction("xor eax, eax"); // null pointer indicates conversion failure + emitter.instruction("mov rdx, -1"); // distinguish failure from a valid empty string + emitter.label("__rt_win_proc_scalar_done"); + emitter.instruction("pop rbp"); // restore caller frame pointer + emitter.instruction("ret"); // return the borrowed scalar string pair +} + +/// Emits a Unicode ordinal case-insensitive comparison for two counted UTF-8 +/// environment names. Returns 1 equal, 0 unequal, or -1 on allocation/UTF error. +fn emit_key_equal(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: Windows proc_open environment key comparison ---"); + emitter.label_global("__rt_win_proc_env_key_equal"); + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable comparison frame + emitter.instruction("sub rsp, 128"); // reserve owned conversions plus native shadow/stack arguments + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // first UTF-8 key pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // first UTF-8 key length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // second UTF-8 key pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // second UTF-8 key length + emitter.instruction("mov QWORD PTR [rbp - 40], 0"); // first narrow staging = null + emitter.instruction("mov QWORD PTR [rbp - 48], 0"); // first wide key = null + emitter.instruction("mov QWORD PTR [rbp - 56], 0"); // second narrow staging = null + emitter.instruction("mov QWORD PTR [rbp - 64], 0"); // second wide key = null + emit_counted_key_conversion(emitter, 8, 16, 40, 48, "first"); + emit_counted_key_conversion(emitter, 24, 32, 56, 64, "second"); + emitter.instruction("mov rcx, QWORD PTR [rbp - 48]"); // first UTF-16 environment name + emitter.instruction("mov rdx, -1"); // first string is NUL terminated + emitter.instruction("mov r8, QWORD PTR [rbp - 64]"); // second UTF-16 environment name + emitter.instruction("mov r9, -1"); // second string is NUL terminated + emitter.instruction("mov QWORD PTR [rsp + 32], 1"); // bIgnoreCase = TRUE + emitter.instruction("call CompareStringOrdinal"); // CSTR_EQUAL=2 for Unicode ordinal equality + emitter.instruction("cmp eax, 2"); // equal result? + emitter.instruction("sete al"); // normalize to one byte + emitter.instruction("movzx eax, al"); // return 0/1 + emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // preserve result across cleanup calls + emitter.instruction("jmp __rt_win_proc_env_key_cleanup"); // release all conversion buffers + emitter.label("__rt_win_proc_env_key_nomem"); + emitter.instruction("mov QWORD PTR [rip + __rt_errno], 12"); // ENOMEM: key conversion allocation failed + emitter.instruction("jmp __rt_win_proc_env_key_fail"); // release partial conversions + emitter.label("__rt_win_proc_env_key_utf8"); + emitter.instruction("mov QWORD PTR [rip + __rt_errno], 84"); // EILSEQ: environment name is not valid UTF-8 + emitter.label("__rt_win_proc_env_key_fail"); + emitter.instruction("mov QWORD PTR [rbp - 72], -1"); // propagate comparison failure + emitter.label("__rt_win_proc_env_key_cleanup"); + for (offset, next) in [(40, "n1"), (48, "w1"), (56, "n2"), (64, "done")] { + emitter.instruction(&format!("mov rax, QWORD PTR [rbp - {offset}]")); // load one optional owned conversion buffer + emitter.instruction("test rax, rax"); // was this buffer allocated? + emitter.instruction(&format!("jz __rt_win_proc_env_key_cleanup_{next}")); // skip null ownership slots + emitter.instruction("call __rt_heap_free"); // release this conversion buffer + emitter.label(&format!("__rt_win_proc_env_key_cleanup_{next}")); + } + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // restore equality/error result + emitter.instruction("add rsp, 128"); // release conversion and native-call scratch + emitter.instruction("pop rbp"); // restore caller frame pointer + emitter.instruction("ret"); // return comparison status +} + +/// Emits one counted UTF-8 to owned UTF-16 conversion within the key comparator. +fn emit_counted_key_conversion( + emitter: &mut Emitter, + pointer_offset: usize, + length_offset: usize, + narrow_offset: usize, + wide_offset: usize, + suffix: &str, +) { + emitter.instruction(&format!("mov rax, QWORD PTR [rbp - {length_offset}]")); // load counted key length + emitter.instruction("inc rax"); // include a NUL terminator + emitter.instruction("jz __rt_win_proc_env_key_nomem"); // reject allocation-size overflow + emitter.instruction("call __rt_heap_alloc"); // allocate narrow NUL staging + emitter.instruction(&format!("mov QWORD PTR [rbp - {narrow_offset}], rax")); // retain the owned staging buffer + emitter.instruction("test rax, rax"); // allocation succeeded? + emitter.instruction("jz __rt_win_proc_env_key_nomem"); // publish ENOMEM + emitter.instruction("mov rdi, rax"); // destination narrow cursor + emitter.instruction(&format!("mov rsi, QWORD PTR [rbp - {pointer_offset}]")); // source key bytes + emitter.instruction(&format!("mov rcx, QWORD PTR [rbp - {length_offset}]")); // source byte count + emitter.instruction("rep movsb"); // copy the complete counted key + emitter.instruction("mov BYTE PTR [rdi], 0"); // append NUL + emitter.instruction(&format!("mov rdi, QWORD PTR [rbp - {narrow_offset}]")); // pass NUL-terminated UTF-8 key + emitter.instruction("call __rt_win_utf8_to_utf16"); // strict UTF-8 conversion + emitter.instruction(&format!("mov QWORD PTR [rbp - {wide_offset}], rax")); // retain owned UTF-16 key + emitter.instruction("test rax, rax"); // conversion succeeded? + emitter.instruction("jz __rt_win_proc_env_key_utf8"); // reject invalid UTF-8 + emitter.comment(&format!("--- converted {suffix} environment key ---")); +} + +/// Emits `__rt_win_proc_options`, which extracts php-src's five documented +/// Windows options as a bit mask. +/// +/// php-src's `get_option()` ignores unknown keys and treats only `true` or a +/// non-zero integer as enabled. Keep that permissive behavior here: malformed +/// option values are false rather than a compiler-specific `EINVAL` failure. +fn emit_options(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: Windows proc_open options marshalling ---"); + emitter.label_global("__rt_win_proc_options"); + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable lookup frame + emitter.instruction("sub rsp, 64"); // reserve five lookup keys and nested-call alignment + emitter.instruction("xor eax, eax"); // default result has no enabled option bits + emitter.instruction("test rdi, rdi"); // null options use PHP defaults + emitter.instruction("jz __rt_win_proc_options_done"); // omitted options have no enabled flags + emitter.instruction("mov rax, QWORD PTR [rdi - 8]"); // inspect the array storage kind + emitter.instruction("and eax, 0xff"); // isolate the low-byte kind + emitter.instruction("cmp eax, 2"); // indexed storage has only numeric, ignored keys + emitter.instruction("je __rt_win_proc_options_done"); // php-src ignores unrecognized numeric entries + emitter.instruction("cmp eax, 3"); // kind 3 identifies associative storage + emitter.instruction("jne __rt_win_proc_options_done"); // non-array values cannot carry recognized option keys + emitter.instruction("mov QWORD PTR [rbp - 8], 0"); // accumulated Windows option bits + emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // preserve the associative options table + emit_option_lookup( + emitter, + "bypass_shell", + &[0x735f737361707962, 0x6c6c6568], + 12, + 1, + ); + emit_option_lookup( + emitter, + "suppress_errors", + &[0x7373657270707573, 0x0073726f7272655f], + 15, + 2, + ); + emit_option_lookup( + emitter, + "blocking_pipes", + &[0x676e696b636f6c62, 0x000073657069705f], + 14, + 4, + ); + emit_option_lookup( + emitter, + "create_process_group", + &[0x705f657461657263, 0x675f737365636f72, 0x70756f72], + 20, + 8, + ); + emit_option_lookup( + emitter, + "create_new_console", + &[0x6e5f657461657263, 0x6f736e6f635f7765, 0x656c], + 18, + 16, + ); + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // return the packed recognized option bits + emitter.label("__rt_win_proc_options_done"); + emitter.instruction("add rsp, 64"); // release lookup scratch + emitter.instruction("pop rbp"); // restore caller frame pointer + emitter.instruction("ret"); // return the packed option bits +} + +/// Emits one `__rt_hash_get` lookup and conditionally sets one PHP Windows +/// process-option bit when its value is `true` or a non-zero integer. +fn emit_option_lookup( + emitter: &mut Emitter, + name: &str, + words: &[u64], + length: usize, + bit: u8, +) { + let suffix = name.replace('_', "_"); + let absent = format!("__rt_win_proc_options_{suffix}_absent"); + let tag_ready = format!("__rt_win_proc_options_{suffix}_tag_ready"); + let enabled = format!("__rt_win_proc_options_{suffix}_enabled"); + let done = format!("__rt_win_proc_options_{suffix}_done"); + emitter.comment(&format!("--- read proc_open option {name} ---")); + for (index, word) in words.iter().enumerate() { + let offset = 16 + index * 8; + emitter.instruction(&format!("movabs rax, 0x{word:016x}")); // materialize eight option-key bytes + emitter.instruction(&format!("mov QWORD PTR [rbp - {offset}], rax")); // store the lookup-key chunk + } + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // associative options hash + emitter.instruction("lea rsi, [rbp - 16]"); // contiguous option-key storage + emitter.instruction(&format!("mov edx, {length}")); // exact option-key byte length + emitter.instruction("call __rt_hash_get"); // rax=found, rdi=value_lo, rsi=value_hi, rcx=value_tag + emitter.instruction("test rax, rax"); // recognized option present? + emitter.instruction(&format!("jz {absent}")); // absent options retain their default false value + emitter.instruction("cmp rcx, 7"); // boxed Mixed payload? + emitter.instruction(&format!("jne {tag_ready}")); // direct hash values already expose their tag + emitter.instruction("mov rax, rdi"); // unbox the nested Mixed cell + emitter.instruction("call __rt_mixed_unbox"); // rax=tag, rdi=value_lo, rdx=value_hi + emitter.instruction("mov rcx, rax"); // normalize the tag for scalar matching + emitter.label(&tag_ready); + emitter.instruction("cmp rcx, 3"); // boolean option value? + emitter.instruction(&format!("je {enabled}")); // booleans use their low payload word + emitter.instruction("cmp rcx, 0"); // integer option value? + emitter.instruction(&format!("jne {done}")); // strings and other values are false in php-src + emitter.label(&enabled); + emitter.instruction("test rdi, rdi"); // true or non-zero integer? + emitter.instruction(&format!("jz {done}")); // false/zero leaves the bit clear + emitter.instruction(&format!("or QWORD PTR [rbp - 8], {bit}")); // enable this recognized Windows option + emitter.label(&done); + emitter.label(&absent); +} + +/// Emits `__rt_win_proc_command_array`, which quotes a runtime indexed array of +/// strings and returns an owned UTF-8 command line in `rax`/`rdx`. +fn emit_command_array(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: Windows proc_open command-array marshalling ---"); + emitter.label_global("__rt_win_proc_command_array"); + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable marshalling frame + emitter.instruction("sub rsp, 128"); // reserve indexed/hash scan state and nested-call alignment + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the command-array payload pointer + emitter.instruction("test rdi, rdi"); // reject a null command array + emitter.instruction("jz __rt_win_proc_command_invalid"); // null cannot name an executable + emitter.instruction("mov rax, QWORD PTR [rdi - 8]"); // load packed indexed-array metadata + emitter.instruction("and eax, 0xff"); // isolate the heap storage kind + emitter.instruction("cmp eax, 2"); // kind 2 identifies indexed storage + emitter.instruction("je __rt_win_proc_command_indexed"); // indexed values preserve their natural order + emitter.instruction("cmp eax, 3"); // kind 3 identifies associative storage + emitter.instruction("jne __rt_win_proc_command_invalid"); // reject non-array runtime storage + emitter.instruction("mov QWORD PTR [rbp - 104], 3"); // associative command arrays ignore keys and iterate insertion-order values + emitter.instruction("call __rt_hash_count"); // count the hash values that become argv entries + emitter.instruction("jmp __rt_win_proc_command_count_ready"); // share empty and argc validation below + emitter.label("__rt_win_proc_command_indexed"); + emitter.instruction("mov QWORD PTR [rbp - 104], 2"); // remember indexed storage for direct element loads + emitter.instruction("mov rax, QWORD PTR [rdi]"); // load argc + emitter.label("__rt_win_proc_command_count_ready"); + emitter.instruction("test rax, rax"); // PHP requires a non-empty command array + emitter.instruction("jz __rt_win_proc_command_invalid"); // empty argv cannot be executed + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // preserve argc + emitter.instruction("cmp QWORD PTR [rbp - 104], 3"); // associative commands do not use an indexed element stride + emitter.instruction("je __rt_win_proc_command_hash_stride"); // their iterator supplies each tagged value directly + emitter.instruction("mov rax, QWORD PTR [rdi + 16]"); // load the runtime element stride + emitter.instruction("cmp rax, 16"); // direct string elements occupy pointer/length pairs + emitter.instruction("je __rt_win_proc_command_stride_ok"); // accept a homogeneous string array + emitter.instruction("cmp rax, 8"); // Mixed elements occupy boxed-pointer words + emitter.instruction("jne __rt_win_proc_command_invalid"); // no other element representation can be argv + emitter.instruction("jmp __rt_win_proc_command_stride_ok"); // retain the validated indexed stride + emitter.label("__rt_win_proc_command_hash_stride"); + emitter.instruction("xor eax, eax"); // associative iteration has no indexed element stride + emitter.label("__rt_win_proc_command_stride_ok"); + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // preserve the element stride + emitter.instruction("mov QWORD PTR [rbp - 88], 0"); // default: argv is not interpreted by cmd.exe + emitter.instruction("mov QWORD PTR [rbp - 32], 0"); // load argv[0] for bounded command-processor detection + emit_load_command_element(emitter, "detect"); + emitter.instruction("test rdx, rdx"); // argv[0] must name a non-empty program + emitter.instruction("jz __rt_win_proc_command_invalid"); // reject an empty executable name + emitter.instruction("call __rt_win_proc_command_uses_cmd"); // detect cmd.exe/cmd/batch basename without path allocation + emitter.instruction("mov QWORD PTR [rbp - 88], rax"); // persist command-processor mode across both passes + emitter.instruction("mov QWORD PTR [rbp - 32], 0"); // sizing-pass element index = 0 + emitter.instruction("mov QWORD PTR [rbp - 40], 0"); // output byte count = 0 + + emitter.label("__rt_win_proc_command_size_loop"); + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the sizing index + emitter.instruction("cmp rax, QWORD PTR [rbp - 16]"); // processed every argv element? + emitter.instruction("jae __rt_win_proc_command_allocate"); // yes, allocate the exact output size + emit_load_command_element(emitter, "size"); + emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // preserve the current argument pointer + emitter.instruction("mov QWORD PTR [rbp - 56], rdx"); // preserve the current argument length + emitter.instruction("cmp QWORD PTR [rbp - 32], 0"); // executable name is the first argv element + emitter.instruction("jne __rt_win_proc_command_first_valid"); // later empty arguments are valid and quoted + emitter.instruction("test rdx, rdx"); // empty executable name? + emitter.instruction("jz __rt_win_proc_command_invalid"); // PHP rejects an empty argv[0] + emitter.label("__rt_win_proc_command_first_valid"); + emitter.instruction("mov QWORD PTR [rbp - 96], 0"); // this argument has no cmd metacharacter yet + emitter.instruction("xor r8d, r8d"); // needs_quotes = false + emitter.instruction("test rdx, rdx"); // empty arguments must be quoted + emitter.instruction("setz r8b"); // remember the empty-argument quoting requirement + emitter.instruction("xor r9d, r9d"); // scan offset = 0 + emitter.label("__rt_win_proc_command_quote_scan"); + emitter.instruction("cmp r9, rdx"); // reached the end of this argument? + emitter.instruction("jae __rt_win_proc_command_quote_known"); // yes, the quote decision is complete + emitter.instruction("movzx eax, BYTE PTR [rdi + r9]"); // load one UTF-8 byte + emitter.instruction("test al, al"); // embedded NUL cannot survive CreateProcessW + emitter.instruction("jz __rt_win_proc_command_invalid"); // reject truncating argv data + emitter.instruction("cmp al, 0x20"); // ASCII space is a CommandLineToArgvW separator + emitter.instruction("je __rt_win_proc_command_mark_quote"); // quote arguments containing spaces + emitter.instruction("cmp al, 0x09"); // tab is the other Windows argv separator + emitter.instruction("je __rt_win_proc_command_mark_quote"); // quote arguments containing tabs + emitter.instruction("cmp al, 0x22"); // embedded double quote needs escaping and outer quotes + emitter.instruction("je __rt_win_proc_command_mark_quote"); // mark the argument for quoted encoding + emitter.instruction("inc r9"); // advance the quote-decision scan + emitter.instruction("jmp __rt_win_proc_command_quote_scan"); // inspect the next byte + emitter.label("__rt_win_proc_command_mark_quote"); + emitter.instruction("mov r8, 1"); // record that this argument needs quotes + emitter.label("__rt_win_proc_command_quote_known"); + emitter.instruction("cmp QWORD PTR [rbp - 32], 0"); // argv[0] is not parsed as a command-processor argument + emitter.instruction("je __rt_win_proc_command_quote_finish"); // retain ordinary executable quoting + emitter.instruction("cmp QWORD PTR [rbp - 88], 0"); // cmd.exe or a batch-file target? + emitter.instruction("je __rt_win_proc_command_quote_finish"); // ordinary executable has no caret escaping + emitter.instruction("mov r8, 1"); // php-src quotes every argument interpreted by cmd.exe + emitter.instruction("xor r9d, r9d"); // scan command-processor metacharacters + emitter.label("__rt_win_proc_command_special_scan"); + emitter.instruction("cmp r9, QWORD PTR [rbp - 56]"); // scanned every byte in this command argument? + emitter.instruction("jae __rt_win_proc_command_quote_finish"); // retain the accumulated special-character flag + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload argument base for the metacharacter scan + emitter.instruction("movzx eax, BYTE PTR [rdi + r9]"); // load one command argument byte + emitter.instruction("cmp al, 0x28"); // opening parenthesis is special to cmd.exe + emitter.instruction("je __rt_win_proc_command_special_found"); // remember it for caret escaping + emitter.instruction("cmp al, 0x29"); // closing parenthesis is special to cmd.exe + emitter.instruction("je __rt_win_proc_command_special_found"); // remember it for caret escaping + emitter.instruction("cmp al, 0x21"); // exclamation mark is special to cmd.exe + emitter.instruction("je __rt_win_proc_command_special_found"); // remember it for caret escaping + emitter.instruction("cmp al, 0x5e"); // caret itself must be escaped + emitter.instruction("je __rt_win_proc_command_special_found"); // remember it for caret escaping + emitter.instruction("cmp al, 0x22"); // a double quote needs cmd.exe escaping too + emitter.instruction("je __rt_win_proc_command_special_found"); // remember it for caret escaping + emitter.instruction("cmp al, 0x3c"); // input redirection marker is special + emitter.instruction("je __rt_win_proc_command_special_found"); // remember it for caret escaping + emitter.instruction("cmp al, 0x3e"); // output redirection marker is special + emitter.instruction("je __rt_win_proc_command_special_found"); // remember it for caret escaping + emitter.instruction("cmp al, 0x26"); // command separator is special + emitter.instruction("je __rt_win_proc_command_special_found"); // remember it for caret escaping + emitter.instruction("cmp al, 0x7c"); // pipeline separator is special + emitter.instruction("je __rt_win_proc_command_special_found"); // remember it for caret escaping + emitter.instruction("cmp al, 0x25"); // environment expansion marker is special + emitter.instruction("je __rt_win_proc_command_special_found"); // remember it for caret escaping + emitter.instruction("inc r9"); // advance the metacharacter scan + emitter.instruction("jmp __rt_win_proc_command_special_scan"); // inspect the next command argument byte + emitter.label("__rt_win_proc_command_special_found"); + emitter.instruction("mov QWORD PTR [rbp - 96], 1"); // enable caret escaping for this entire argument + emitter.instruction("inc r9"); // continue scanning after the metacharacter + emitter.instruction("jmp __rt_win_proc_command_special_scan"); // find later metacharacters too + emitter.label("__rt_win_proc_command_quote_finish"); + emitter.instruction("mov QWORD PTR [rbp - 64], r8"); // preserve needs_quotes for the sizing walk + emitter.instruction("test r8, r8"); // unquoted arguments copy byte-for-byte + emitter.instruction("jnz __rt_win_proc_command_size_quoted"); // quoted arguments need backslash expansion + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload accumulated output size + emitter.instruction("add rax, rdx"); // add the raw argument length + emitter.instruction("jc __rt_win_proc_command_nomem"); // size_t overflow is reported as ENOMEM + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // persist the enlarged output size + emitter.instruction("jmp __rt_win_proc_command_size_separator"); // account for the separator + + emitter.label("__rt_win_proc_command_size_quoted"); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload accumulated output size + emitter.instruction("add rax, 2"); // include opening and closing quotes + emitter.instruction("jc __rt_win_proc_command_nomem"); // reject quote-size overflow + emitter.instruction("cmp QWORD PTR [rbp - 96], 0"); // this cmd.exe argument has metacharacters? + emitter.instruction("je __rt_win_proc_command_size_outer_ready"); // ordinary quotes need no caret prefix + emitter.instruction("add rax, 2"); // prefix both surrounding quotes with carets + emitter.instruction("jc __rt_win_proc_command_nomem"); // reject caret-size overflow + emitter.label("__rt_win_proc_command_size_outer_ready"); + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // persist quote bytes + emitter.instruction("xor r9d, r9d"); // byte offset = 0 + emitter.instruction("xor r10d, r10d"); // pending backslash run = 0 + emitter.label("__rt_win_proc_command_size_bytes"); + emitter.instruction("cmp r9, QWORD PTR [rbp - 56]"); // consumed the whole argument? + emitter.instruction("jae __rt_win_proc_command_size_trailing"); // double trailing backslashes before the close quote + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the argument pointer + emitter.instruction("movzx eax, BYTE PTR [rdi + r9]"); // load the next argument byte + emitter.instruction("cmp al, 0x5c"); // backslash? + emitter.instruction("jne __rt_win_proc_command_size_non_slash"); // flush the accumulated run for another byte + emitter.instruction("inc r10"); // extend the pending backslash run + emitter.instruction("inc r9"); // consume this backslash + emitter.instruction("jmp __rt_win_proc_command_size_bytes"); // keep scanning the run + emitter.label("__rt_win_proc_command_size_non_slash"); + emitter.instruction("cmp al, 0x22"); // embedded quote doubles preceding backslashes + emitter.instruction("jne __rt_win_proc_command_size_plain"); // plain bytes preserve the run unchanged + emitter.instruction("shl r10, 1"); // double the preceding backslashes + emitter.instruction("inc r10"); // add the escape slash before the quote + emitter.label("__rt_win_proc_command_size_plain"); + emitter.instruction("cmp QWORD PTR [rbp - 96], 0"); // command-processor caret escaping enabled? + emitter.instruction("je __rt_win_proc_command_size_no_caret"); // ordinary quoted bytes need no extra prefix + emitter.instruction("cmp al, 0x28"); // opening parenthesis is special to cmd.exe + emitter.instruction("je __rt_win_proc_command_size_caret"); // include its caret prefix + emitter.instruction("cmp al, 0x29"); // closing parenthesis is special to cmd.exe + emitter.instruction("je __rt_win_proc_command_size_caret"); // include its caret prefix + emitter.instruction("cmp al, 0x21"); // exclamation mark is special to cmd.exe + emitter.instruction("je __rt_win_proc_command_size_caret"); // include its caret prefix + emitter.instruction("cmp al, 0x5e"); // caret itself is special to cmd.exe + emitter.instruction("je __rt_win_proc_command_size_caret"); // include its caret prefix + emitter.instruction("cmp al, 0x22"); // quote is special to cmd.exe + emitter.instruction("je __rt_win_proc_command_size_caret"); // include its caret prefix + emitter.instruction("cmp al, 0x3c"); // input redirection marker is special + emitter.instruction("je __rt_win_proc_command_size_caret"); // include its caret prefix + emitter.instruction("cmp al, 0x3e"); // output redirection marker is special + emitter.instruction("je __rt_win_proc_command_size_caret"); // include its caret prefix + emitter.instruction("cmp al, 0x26"); // command separator is special + emitter.instruction("je __rt_win_proc_command_size_caret"); // include its caret prefix + emitter.instruction("cmp al, 0x7c"); // pipeline separator is special + emitter.instruction("je __rt_win_proc_command_size_caret"); // include its caret prefix + emitter.instruction("cmp al, 0x25"); // environment expansion marker is special + emitter.instruction("jne __rt_win_proc_command_size_no_caret"); // non-special byte has no caret prefix + emitter.label("__rt_win_proc_command_size_caret"); + emitter.instruction("inc r10"); // count the caret before this metacharacter + emitter.label("__rt_win_proc_command_size_no_caret"); + emitter.instruction("inc r10"); // include the current non-backslash byte + emitter.instruction("add QWORD PTR [rbp - 40], r10"); // append the encoded run and byte length + emitter.instruction("jc __rt_win_proc_command_nomem"); // reject accumulated-size overflow + emitter.instruction("xor r10d, r10d"); // reset the pending slash run + emitter.instruction("inc r9"); // consume the current byte + emitter.instruction("jmp __rt_win_proc_command_size_bytes"); // continue sizing this argument + emitter.label("__rt_win_proc_command_size_trailing"); + emitter.instruction("shl r10, 1"); // trailing slashes are doubled before a closing quote + emitter.instruction("add QWORD PTR [rbp - 40], r10"); // include the expanded trailing run + emitter.instruction("jc __rt_win_proc_command_nomem"); // reject final-run overflow + + emitter.label("__rt_win_proc_command_size_separator"); + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload element index + emitter.instruction("inc rax"); // advance to the next argv element + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // persist the next index + emitter.instruction("cmp rax, QWORD PTR [rbp - 16]"); // is another argument coming? + emitter.instruction("jae __rt_win_proc_command_size_loop"); // no separator after the final argument + emitter.instruction("add QWORD PTR [rbp - 40], 1"); // include one inter-argument space + emitter.instruction("jc __rt_win_proc_command_nomem"); // reject separator overflow + emitter.instruction("jmp __rt_win_proc_command_size_loop"); // size the next argument + + emitter.label("__rt_win_proc_command_allocate"); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // exact command-line byte length + emitter.instruction("inc rax"); // include a defensive trailing NUL + emitter.instruction("jz __rt_win_proc_command_nomem"); // wrapped allocation size is invalid + emitter.instruction("call __rt_heap_alloc"); // allocate the raw command-line staging buffer + emitter.instruction("test rax, rax"); // allocation succeeded? + emitter.instruction("jz __rt_win_proc_command_nomem"); // publish ENOMEM on allocation failure + emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // preserve the owned output base + emitter.instruction("mov QWORD PTR [rbp - 80], rax"); // initialize the output cursor + emitter.instruction("mov QWORD PTR [rbp - 32], 0"); // writing-pass element index = 0 + + emitter.label("__rt_win_proc_command_write_loop"); + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload writing index + emitter.instruction("cmp rax, QWORD PTR [rbp - 16]"); // emitted every argument? + emitter.instruction("jae __rt_win_proc_command_done"); // terminate and return the completed buffer + emit_load_command_element(emitter, "write"); + emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // preserve argument pointer for writing + emitter.instruction("mov QWORD PTR [rbp - 56], rdx"); // preserve argument length for writing + emitter.instruction("mov QWORD PTR [rbp - 96], 0"); // this argument has no cmd metacharacter yet + emitter.instruction("xor r8d, r8d"); // needs_quotes = false + emitter.instruction("test rdx, rdx"); // empty strings require quotes + emitter.instruction("setz r8b"); // seed the quoting decision + emitter.instruction("xor r9d, r9d"); // decision scan offset = 0 + emitter.label("__rt_win_proc_command_write_quote_scan"); + emitter.instruction("cmp r9, rdx"); // finished the decision scan? + emitter.instruction("jae __rt_win_proc_command_write_quote_known"); // proceed with the selected encoding + emitter.instruction("movzx eax, BYTE PTR [rdi + r9]"); // load one source byte + emitter.instruction("cmp al, 0x20"); // space separator? + emitter.instruction("je __rt_win_proc_command_write_mark_quote"); // quote the argument + emitter.instruction("cmp al, 0x09"); // tab separator? + emitter.instruction("je __rt_win_proc_command_write_mark_quote"); // quote the argument + emitter.instruction("cmp al, 0x22"); // embedded quote? + emitter.instruction("je __rt_win_proc_command_write_mark_quote"); // quote and escape the argument + emitter.instruction("inc r9"); // inspect the next source byte + emitter.instruction("jmp __rt_win_proc_command_write_quote_scan"); // continue the decision scan + emitter.label("__rt_win_proc_command_write_mark_quote"); + emitter.instruction("mov r8, 1"); // select quoted encoding + emitter.label("__rt_win_proc_command_write_quote_known"); + emitter.instruction("cmp QWORD PTR [rbp - 32], 0"); // argv[0] is not parsed as a command-processor argument + emitter.instruction("je __rt_win_proc_command_write_quote_finish"); // retain ordinary executable quoting + emitter.instruction("cmp QWORD PTR [rbp - 88], 0"); // cmd.exe or a batch-file target? + emitter.instruction("je __rt_win_proc_command_write_quote_finish"); // ordinary executable has no caret escaping + emitter.instruction("mov r8, 1"); // php-src quotes every argument interpreted by cmd.exe + emitter.instruction("xor r9d, r9d"); // scan command-processor metacharacters + emitter.label("__rt_win_proc_command_write_special_scan"); + emitter.instruction("cmp r9, QWORD PTR [rbp - 56]"); // scanned every byte in this command argument? + emitter.instruction("jae __rt_win_proc_command_write_quote_finish"); // retain the accumulated special-character flag + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload argument base for the metacharacter scan + emitter.instruction("movzx eax, BYTE PTR [rdi + r9]"); // load one command argument byte + emitter.instruction("cmp al, 0x28"); // opening parenthesis is special to cmd.exe + emitter.instruction("je __rt_win_proc_command_write_special_found"); // remember it for caret escaping + emitter.instruction("cmp al, 0x29"); // closing parenthesis is special to cmd.exe + emitter.instruction("je __rt_win_proc_command_write_special_found"); // remember it for caret escaping + emitter.instruction("cmp al, 0x21"); // exclamation mark is special to cmd.exe + emitter.instruction("je __rt_win_proc_command_write_special_found"); // remember it for caret escaping + emitter.instruction("cmp al, 0x5e"); // caret itself must be escaped + emitter.instruction("je __rt_win_proc_command_write_special_found"); // remember it for caret escaping + emitter.instruction("cmp al, 0x22"); // a double quote needs cmd.exe escaping too + emitter.instruction("je __rt_win_proc_command_write_special_found"); // remember it for caret escaping + emitter.instruction("cmp al, 0x3c"); // input redirection marker is special + emitter.instruction("je __rt_win_proc_command_write_special_found"); // remember it for caret escaping + emitter.instruction("cmp al, 0x3e"); // output redirection marker is special + emitter.instruction("je __rt_win_proc_command_write_special_found"); // remember it for caret escaping + emitter.instruction("cmp al, 0x26"); // command separator is special + emitter.instruction("je __rt_win_proc_command_write_special_found"); // remember it for caret escaping + emitter.instruction("cmp al, 0x7c"); // pipeline separator is special + emitter.instruction("je __rt_win_proc_command_write_special_found"); // remember it for caret escaping + emitter.instruction("cmp al, 0x25"); // environment expansion marker is special + emitter.instruction("je __rt_win_proc_command_write_special_found"); // remember it for caret escaping + emitter.instruction("inc r9"); // advance the metacharacter scan + emitter.instruction("jmp __rt_win_proc_command_write_special_scan"); // inspect the next command argument byte + emitter.label("__rt_win_proc_command_write_special_found"); + emitter.instruction("mov QWORD PTR [rbp - 96], 1"); // enable caret escaping for this entire argument + emitter.instruction("inc r9"); // continue scanning after the metacharacter + emitter.instruction("jmp __rt_win_proc_command_write_special_scan"); // find later metacharacters too + emitter.label("__rt_win_proc_command_write_quote_finish"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // restore source pointer after the scan + emitter.instruction("mov rsi, QWORD PTR [rbp - 80]"); // load destination cursor + emitter.instruction("test r8, r8"); // quoted or raw copy? + emitter.instruction("jnz __rt_win_proc_command_write_quoted"); // expand quoted arguments + emitter.instruction("mov rcx, QWORD PTR [rbp - 56]"); // raw argument byte count + emitter.instruction("xchg rdi, rsi"); // rep movsb requires source in rsi and destination in rdi + emitter.instruction("rep movsb"); // copy an unquoted argument byte-for-byte + emitter.instruction("mov rsi, rdi"); // restore the destination cursor convention used below + emitter.instruction("jmp __rt_win_proc_command_write_separator"); // append the optional separator + + emitter.label("__rt_win_proc_command_write_quoted"); + emitter.instruction("cmp QWORD PTR [rbp - 96], 0"); // cmd.exe metacharacter escaping required? + emitter.instruction("je __rt_win_proc_command_write_open_quote"); // ordinary quote starts the encoded argument + emitter.instruction("mov BYTE PTR [rsi], 0x5e"); // caret-escape the opening quote for cmd.exe + emitter.instruction("inc rsi"); // advance past the opening quote caret + emitter.label("__rt_win_proc_command_write_open_quote"); + emitter.instruction("mov BYTE PTR [rsi], 0x22"); // opening quote + emitter.instruction("inc rsi"); // advance destination past the quote + emitter.instruction("xor r9d, r9d"); // source offset = 0 + emitter.instruction("xor r10d, r10d"); // pending slash count = 0 + emitter.label("__rt_win_proc_command_write_bytes"); + emitter.instruction("cmp r9, QWORD PTR [rbp - 56]"); // consumed the argument? + emitter.instruction("jae __rt_win_proc_command_write_trailing"); // flush trailing slashes and close quotes + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload source base + emitter.instruction("movzx eax, BYTE PTR [rdi + r9]"); // load current byte + emitter.instruction("cmp al, 0x5c"); // backslash? + emitter.instruction("jne __rt_win_proc_command_write_non_slash"); // delay slash emission until the following byte is known + emitter.instruction("inc r10"); // count this pending slash + emitter.instruction("inc r9"); // consume it + emitter.instruction("jmp __rt_win_proc_command_write_bytes"); // continue the slash run + emitter.label("__rt_win_proc_command_write_non_slash"); + emitter.instruction("mov rcx, r10"); // begin with the literal pending slash count + emitter.instruction("cmp al, 0x22"); // embedded quote? + emitter.instruction("jne __rt_win_proc_command_write_slashes"); // plain bytes retain the slash run + emitter.instruction("shl rcx, 1"); // double slashes before a quote + emitter.instruction("inc rcx"); // add the quote escape slash + emitter.label("__rt_win_proc_command_write_slashes"); + emitter.instruction("test rcx, rcx"); // is there a slash run to flush? + emitter.instruction("jz __rt_win_proc_command_write_byte"); // no, copy the current byte directly + emitter.label("__rt_win_proc_command_write_slash_loop"); + emitter.instruction("mov BYTE PTR [rsi], 0x5c"); // write one required slash + emitter.instruction("inc rsi"); // advance output + emitter.instruction("loop __rt_win_proc_command_write_slash_loop"); // emit the complete expanded slash run + emitter.label("__rt_win_proc_command_write_byte"); + emitter.instruction("cmp QWORD PTR [rbp - 96], 0"); // command-processor caret escaping enabled? + emitter.instruction("je __rt_win_proc_command_write_plain_byte"); // ordinary quoted bytes need no extra prefix + emitter.instruction("cmp al, 0x28"); // opening parenthesis is special to cmd.exe + emitter.instruction("je __rt_win_proc_command_write_caret"); // emit its caret prefix + emitter.instruction("cmp al, 0x29"); // closing parenthesis is special to cmd.exe + emitter.instruction("je __rt_win_proc_command_write_caret"); // emit its caret prefix + emitter.instruction("cmp al, 0x21"); // exclamation mark is special to cmd.exe + emitter.instruction("je __rt_win_proc_command_write_caret"); // emit its caret prefix + emitter.instruction("cmp al, 0x5e"); // caret itself is special to cmd.exe + emitter.instruction("je __rt_win_proc_command_write_caret"); // emit its caret prefix + emitter.instruction("cmp al, 0x22"); // quote is special to cmd.exe + emitter.instruction("je __rt_win_proc_command_write_caret"); // emit its caret prefix + emitter.instruction("cmp al, 0x3c"); // input redirection marker is special + emitter.instruction("je __rt_win_proc_command_write_caret"); // emit its caret prefix + emitter.instruction("cmp al, 0x3e"); // output redirection marker is special + emitter.instruction("je __rt_win_proc_command_write_caret"); // emit its caret prefix + emitter.instruction("cmp al, 0x26"); // command separator is special + emitter.instruction("je __rt_win_proc_command_write_caret"); // emit its caret prefix + emitter.instruction("cmp al, 0x7c"); // pipeline separator is special + emitter.instruction("je __rt_win_proc_command_write_caret"); // emit its caret prefix + emitter.instruction("cmp al, 0x25"); // environment expansion marker is special + emitter.instruction("jne __rt_win_proc_command_write_plain_byte"); // non-special byte has no caret prefix + emitter.label("__rt_win_proc_command_write_caret"); + emitter.instruction("mov BYTE PTR [rsi], 0x5e"); // write the caret before this metacharacter + emitter.instruction("inc rsi"); // advance after the caret prefix + emitter.label("__rt_win_proc_command_write_plain_byte"); + emitter.instruction("mov BYTE PTR [rsi], al"); // copy the non-backslash byte + emitter.instruction("inc rsi"); // advance output + emitter.instruction("xor r10d, r10d"); // reset pending slash count + emitter.instruction("inc r9"); // consume the current source byte + emitter.instruction("jmp __rt_win_proc_command_write_bytes"); // continue encoding + emitter.label("__rt_win_proc_command_write_trailing"); + emitter.instruction("lea rcx, [r10 + r10]"); // double trailing slashes before the closing quote + emitter.label("__rt_win_proc_command_write_trailing_slashes"); + emitter.instruction("test rcx, rcx"); // any trailing slash left? + emitter.instruction("jz __rt_win_proc_command_write_close"); // no, append the closing quote + emitter.instruction("mov BYTE PTR [rsi], 0x5c"); // write one doubled trailing slash + emitter.instruction("inc rsi"); // advance output + emitter.instruction("dec rcx"); // consume one slash + emitter.instruction("jmp __rt_win_proc_command_write_trailing_slashes"); // flush the full run + emitter.label("__rt_win_proc_command_write_close"); + emitter.instruction("cmp QWORD PTR [rbp - 96], 0"); // cmd.exe metacharacter escaping required? + emitter.instruction("je __rt_win_proc_command_write_close_quote"); // ordinary quote closes the encoded argument + emitter.instruction("mov BYTE PTR [rsi], 0x5e"); // caret-escape the closing quote for cmd.exe + emitter.instruction("inc rsi"); // advance past the closing quote caret + emitter.label("__rt_win_proc_command_write_close_quote"); + emitter.instruction("mov BYTE PTR [rsi], 0x22"); // closing quote + emitter.instruction("inc rsi"); // advance output past it + + emitter.label("__rt_win_proc_command_write_separator"); + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload current element index + emitter.instruction("inc rax"); // advance to the next element + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // persist the writing index + emitter.instruction("cmp rax, QWORD PTR [rbp - 16]"); // is this the final argument? + emitter.instruction("jae __rt_win_proc_command_store_cursor"); // omit separator after the final argument + emitter.instruction("mov BYTE PTR [rsi], 0x20"); // append one inter-argument space + emitter.instruction("inc rsi"); // advance destination + emitter.label("__rt_win_proc_command_store_cursor"); + emitter.instruction("mov QWORD PTR [rbp - 80], rsi"); // persist destination cursor + emitter.instruction("jmp __rt_win_proc_command_write_loop"); // emit the next argument + + emitter.label("__rt_win_proc_command_done"); + emitter.instruction("mov rax, QWORD PTR [rbp - 80]"); // load the final output cursor + emitter.instruction("mov BYTE PTR [rax], 0"); // append the defensive NUL terminator + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // return the owned output base + emitter.instruction("mov rdx, QWORD PTR [rbp - 40]"); // return the counted byte length + emitter.instruction("add rsp, 128"); // release marshalling state + emitter.instruction("pop rbp"); // restore caller frame pointer + emitter.instruction("ret"); // return the command-line pair + + emitter.label("__rt_win_proc_command_invalid"); + emitter.instruction("mov QWORD PTR [rip + __rt_errno], 22"); // EINVAL: invalid argv shape or element + emitter.instruction("jmp __rt_win_proc_command_fail"); // return a null buffer + emitter.label("__rt_win_proc_command_nomem"); + emitter.instruction("mov QWORD PTR [rip + __rt_errno], 12"); // ENOMEM: allocation or size overflow + emitter.label("__rt_win_proc_command_fail"); + emitter.instruction("xor eax, eax"); // null output pointer signals failure + emitter.instruction("xor edx, edx"); // failed output has zero length + emitter.instruction("add rsp, 128"); // release marshalling state + emitter.instruction("pop rbp"); // restore caller frame pointer + emitter.instruction("ret"); // return failure +} + +/// Emits the shared load sequence for one command value. Indexed arrays use +/// their stamped element representation; associative arrays ignore keys and +/// re-walk insertion-order values, matching php-src's `ZEND_HASH_FOREACH_VAL`. +fn emit_load_command_element(emitter: &mut Emitter, suffix: &str) { + let hash = format!("__rt_win_proc_command_{suffix}_hash"); + let hash_loop = format!("__rt_win_proc_command_{suffix}_hash_loop"); + let hash_value = format!("__rt_win_proc_command_{suffix}_hash_value"); + let mixed = format!("__rt_win_proc_command_{suffix}_mixed"); + let scalar = format!("__rt_win_proc_command_{suffix}_scalar"); + let ready = format!("__rt_win_proc_command_{suffix}_ready"); + emitter.instruction("cmp QWORD PTR [rbp - 104], 3"); // associative commands must iterate their values rather than indexed slots + emitter.instruction(&format!("je {hash}")); // associative keys are intentionally ignored by php-src + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // load the current element index + emitter.instruction("imul rax, QWORD PTR [rbp - 24]"); // scale by the runtime element stride + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the indexed-array payload + emitter.instruction("add rdi, 24"); // skip the indexed-array header + emitter.instruction("add rdi, rax"); // address the selected element + emitter.instruction("cmp QWORD PTR [rbp - 24], 8"); // direct string elements use pointer/length pairs + emitter.instruction(&format!("je {mixed}")); // scalar or Mixed element conversion uses the value-type stamp + emitter.instruction("mov rdx, QWORD PTR [rdi + 8]"); // direct string length + emitter.instruction("mov rdi, QWORD PTR [rdi]"); // direct string pointer + emitter.instruction(&format!("jmp {ready}")); // join the validated string path + emitter.label(&mixed); + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the indexed-array base for its value-type stamp + emitter.instruction("mov r10, QWORD PTR [r10 - 8]"); // inspect packed indexed-array metadata + emitter.instruction("shr r10, 8"); // move the value-type tag into the low byte + emitter.instruction("and r10, 0x7f"); // isolate the storage value-type tag + emitter.instruction("cmp r10, 7"); // boxed Mixed element representation? + emitter.instruction(&format!("je {mixed}_unbox")); // unbox the nested cell before scalar conversion + emitter.instruction("cmp r10, 0"); // direct integer elements need scalar conversion + emitter.instruction(&format!("je {scalar}")); // pass direct integers to the shared PHP converter + emitter.instruction("cmp r10, 2"); // direct float elements need scalar conversion + emitter.instruction(&format!("je {scalar}")); // pass direct floats to the shared PHP converter + emitter.instruction("cmp r10, 3"); // direct boolean elements need scalar conversion + emitter.instruction(&format!("je {scalar}")); // pass direct booleans to the shared PHP converter + emitter.instruction("cmp r10, 8"); // direct null elements need scalar conversion + emitter.instruction("jne __rt_win_proc_command_invalid"); // arrays/objects/resources cannot name command arguments + emitter.label(&scalar); + emitter.instruction("mov rsi, QWORD PTR [rdi]"); // load the direct scalar payload + emitter.instruction("mov rdi, r10"); // pass its runtime value tag to the scalar converter + emitter.instruction("xor edx, edx"); // direct scalar slots have no high payload word + emitter.instruction("call __rt_win_proc_scalar_string"); // apply PHP scalar-to-string conversion + emitter.instruction("cmp rdx, -1"); // unsupported scalar conversion? + emitter.instruction("je __rt_win_proc_command_invalid"); // retain the converter's EINVAL result + emitter.instruction(&format!("jmp {ready}")); // expose the converted string pair + emitter.label(&format!("{mixed}_unbox")); + emitter.instruction("mov rax, QWORD PTR [rdi]"); // load the boxed Mixed element pointer + emitter.instruction("call __rt_mixed_unbox"); // rax=tag, rdi=payload pointer, rdx=payload length + emitter.instruction("mov rsi, rdi"); // move the unboxed low payload into scalar-converter input + emitter.instruction("mov rdi, rax"); // move the unboxed runtime tag into scalar-converter input + emitter.instruction("call __rt_win_proc_scalar_string"); // apply PHP scalar-to-string conversion + emitter.instruction("cmp rdx, -1"); // unsupported Mixed payload? + emitter.instruction("je __rt_win_proc_command_invalid"); // retain the converter's EINVAL result + emitter.instruction(&format!("jmp {ready}")); // expose the converted string pair + emitter.label(&hash); + emitter.instruction("mov QWORD PTR [rbp - 112], 0"); // restart the insertion-order hash cursor for this argv position + emitter.instruction("mov QWORD PTR [rbp - 120], 0"); // hash value index = 0 + emitter.label(&hash_loop); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // pass the associative command hash to its iterator + emitter.instruction("mov rsi, QWORD PTR [rbp - 112]"); // pass the iterator cursor + emitter.instruction("call __rt_hash_iter_next"); // fetch key plus the next insertion-order value triple + emitter.instruction("cmp rax, -1"); // did the hash end before its advertised count? + emitter.instruction("je __rt_win_proc_command_invalid"); // malformed storage cannot safely produce argv + emitter.instruction("mov QWORD PTR [rbp - 112], rax"); // persist the next iterator cursor + emitter.instruction("mov rax, QWORD PTR [rbp - 120]"); // reload the current hash value index + emitter.instruction("cmp rax, QWORD PTR [rbp - 32]"); // did this value correspond to the requested argv element? + emitter.instruction(&format!("je {hash_value}")); // convert this hash value and ignore its key + emitter.instruction("inc QWORD PTR [rbp - 120]"); // advance to the next hash value + emitter.instruction(&format!("jmp {hash_loop}")); // continue until the requested value is reached + emitter.label(&hash_value); + emitter.instruction("mov rdi, r9"); // pass the hash value's runtime tag to the scalar converter + emitter.instruction("mov rsi, rcx"); // pass the hash value's low payload word + emitter.instruction("mov rdx, r8"); // pass the hash value's high payload word + emitter.instruction("call __rt_win_proc_scalar_string"); // convert the associative value with PHP rules + emitter.instruction("cmp rdx, -1"); // unsupported hash value? + emitter.instruction("je __rt_win_proc_command_invalid"); // retain the converter's EINVAL result + emitter.label(&ready); +} + +/// Emits the bounded command-processor detector used by runtime argv marshalling. +/// +/// Input is a counted UTF-8 `rdi`/`rdx` argv[0] pair; `rax` is one when its +/// final slash-separated component is `cmd`, `cmd.exe`, or ends in `.bat` or +/// `.cmd` under ASCII case-insensitive comparison. This intentionally avoids +/// php-src's `GetLongPathNameW`/`GetFullPathNameW` allocation and resolution +/// path while matching the direct literal path's bounded spelling contract. +fn emit_command_uses_cmd(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: Windows proc_open command-processor detection ---"); + emitter.label_global("__rt_win_proc_command_uses_cmd"); + emitter.instruction("xor eax, eax"); // default result: ordinary executable + emitter.instruction("xor ecx, ecx"); // scan argv[0] from its first byte + emitter.instruction("xor r8d, r8d"); // basename begins at argv[0] initially + emitter.label("__rt_win_proc_command_uses_cmd_scan"); + emitter.instruction("cmp rcx, rdx"); // consumed the complete executable spelling? + emitter.instruction("jae __rt_win_proc_command_uses_cmd_basename"); // inspect its final path component + emitter.instruction("movzx r9d, BYTE PTR [rdi + rcx]"); // load one executable-name byte + emitter.instruction("cmp r9b, 0x5c"); // Windows path separator? + emitter.instruction("je __rt_win_proc_command_uses_cmd_separator"); // begin a new basename after it + emitter.instruction("cmp r9b, 0x2f"); // accept forward slashes in portable source spellings + emitter.instruction("je __rt_win_proc_command_uses_cmd_separator"); // begin a new basename after it + emitter.instruction("inc rcx"); // continue through this path component + emitter.instruction("jmp __rt_win_proc_command_uses_cmd_scan"); // inspect the next executable-name byte + emitter.label("__rt_win_proc_command_uses_cmd_separator"); + emitter.instruction("lea r8, [rcx + 1]"); // basename begins just after the latest separator + emitter.instruction("inc rcx"); // continue scanning after the separator + emitter.instruction("jmp __rt_win_proc_command_uses_cmd_scan"); // find the final path component + emitter.label("__rt_win_proc_command_uses_cmd_basename"); + emitter.instruction("sub rdx, r8"); // isolate the basename byte length + emitter.instruction("add rdi, r8"); // point at the final path component + emitter.instruction("cmp rdx, 3"); // bare `cmd` has three bytes + emitter.instruction("jne __rt_win_proc_command_uses_cmd_exe"); // try cmd.exe or a batch extension + emitter.instruction("movzx ecx, BYTE PTR [rdi]"); // load the first bare command byte + emitter.instruction("or cl, 0x20"); // ASCII case-fold it + emitter.instruction("cmp cl, 0x63"); // expect `c` + emitter.instruction("jne __rt_win_proc_command_uses_cmd_exe"); // not the command processor + emitter.instruction("movzx ecx, BYTE PTR [rdi + 1]"); // load the second bare command byte + emitter.instruction("or cl, 0x20"); // ASCII case-fold it + emitter.instruction("cmp cl, 0x6d"); // expect `m` + emitter.instruction("jne __rt_win_proc_command_uses_cmd_exe"); // not the command processor + emitter.instruction("movzx ecx, BYTE PTR [rdi + 2]"); // load the third bare command byte + emitter.instruction("or cl, 0x20"); // ASCII case-fold it + emitter.instruction("cmp cl, 0x64"); // expect `d` + emitter.instruction("jne __rt_win_proc_command_uses_cmd_exe"); // not the command processor + emitter.instruction("mov eax, 1"); // bare cmd.exe spelling is handled by the command processor + emitter.instruction("ret"); // return the positive detection result + emitter.label("__rt_win_proc_command_uses_cmd_exe"); + emitter.instruction("cmp rdx, 7"); // cmd.exe has seven bytes + emitter.instruction("jne __rt_win_proc_command_uses_cmd_extension"); // inspect batch-file extensions instead + emitter.instruction("movzx ecx, BYTE PTR [rdi]"); // load cmd.exe byte zero + emitter.instruction("or cl, 0x20"); // ASCII case-fold it + emitter.instruction("cmp cl, 0x63"); // expect `c` + emitter.instruction("jne __rt_win_proc_command_uses_cmd_extension"); // not cmd.exe + emitter.instruction("movzx ecx, BYTE PTR [rdi + 1]"); // load cmd.exe byte one + emitter.instruction("or cl, 0x20"); // ASCII case-fold it + emitter.instruction("cmp cl, 0x6d"); // expect `m` + emitter.instruction("jne __rt_win_proc_command_uses_cmd_extension"); // not cmd.exe + emitter.instruction("movzx ecx, BYTE PTR [rdi + 2]"); // load cmd.exe byte two + emitter.instruction("or cl, 0x20"); // ASCII case-fold it + emitter.instruction("cmp cl, 0x64"); // expect `d` + emitter.instruction("jne __rt_win_proc_command_uses_cmd_extension"); // not cmd.exe + emitter.instruction("cmp BYTE PTR [rdi + 3], 0x2e"); // cmd.exe requires its literal dot + emitter.instruction("jne __rt_win_proc_command_uses_cmd_extension"); // not cmd.exe + emitter.instruction("movzx ecx, BYTE PTR [rdi + 4]"); // load cmd.exe extension byte zero + emitter.instruction("or cl, 0x20"); // ASCII case-fold it + emitter.instruction("cmp cl, 0x65"); // expect `e` + emitter.instruction("jne __rt_win_proc_command_uses_cmd_extension"); // not cmd.exe + emitter.instruction("movzx ecx, BYTE PTR [rdi + 5]"); // load cmd.exe extension byte one + emitter.instruction("or cl, 0x20"); // ASCII case-fold it + emitter.instruction("cmp cl, 0x78"); // expect `x` + emitter.instruction("jne __rt_win_proc_command_uses_cmd_extension"); // not cmd.exe + emitter.instruction("movzx ecx, BYTE PTR [rdi + 6]"); // load cmd.exe extension byte two + emitter.instruction("or cl, 0x20"); // ASCII case-fold it + emitter.instruction("cmp cl, 0x65"); // expect `e` + emitter.instruction("jne __rt_win_proc_command_uses_cmd_extension"); // not cmd.exe + emitter.instruction("mov eax, 1"); // cmd.exe is interpreted by the command processor + emitter.instruction("ret"); // return the positive detection result + emitter.label("__rt_win_proc_command_uses_cmd_extension"); + emitter.instruction("cmp rdx, 4"); // batch extensions require at least four bytes + emitter.instruction("jb __rt_win_proc_command_uses_cmd_done"); // shorter names cannot be batch files + emitter.instruction("lea r8, [rdi + rdx - 4]"); // point at the final four-byte extension + emitter.instruction("cmp BYTE PTR [r8], 0x2e"); // batch extensions begin with a dot + emitter.instruction("jne __rt_win_proc_command_uses_cmd_done"); // not a batch-file extension + emitter.instruction("movzx ecx, BYTE PTR [r8 + 1]"); // load extension byte one + emitter.instruction("or cl, 0x20"); // ASCII case-fold it + emitter.instruction("cmp cl, 0x62"); // `.bat` begins with b + emitter.instruction("je __rt_win_proc_command_uses_cmd_bat"); // verify the remaining batch suffix + emitter.instruction("cmp cl, 0x63"); // `.cmd` begins with c + emitter.instruction("jne __rt_win_proc_command_uses_cmd_done"); // neither supported batch suffix + emitter.instruction("movzx ecx, BYTE PTR [r8 + 2]"); // load `.cmd` byte two + emitter.instruction("or cl, 0x20"); // ASCII case-fold it + emitter.instruction("cmp cl, 0x6d"); // expect m + emitter.instruction("jne __rt_win_proc_command_uses_cmd_done"); // not `.cmd` + emitter.instruction("movzx ecx, BYTE PTR [r8 + 3]"); // load final `.cmd` suffix byte + emitter.instruction("or cl, 0x20"); // ASCII case-fold it + emitter.instruction("cmp cl, 0x64"); // `.cmd` ends in d + emitter.instruction("jne __rt_win_proc_command_uses_cmd_done"); // not `.cmd` + emitter.instruction("jmp __rt_win_proc_command_uses_cmd_yes"); // `.cmd` is a command-processor program + emitter.label("__rt_win_proc_command_uses_cmd_bat"); + emitter.instruction("movzx ecx, BYTE PTR [r8 + 2]"); // load `.bat` byte two + emitter.instruction("or cl, 0x20"); // ASCII case-fold it + emitter.instruction("cmp cl, 0x61"); // expect a + emitter.instruction("jne __rt_win_proc_command_uses_cmd_done"); // not `.bat` + emitter.instruction("movzx ecx, BYTE PTR [r8 + 3]"); // load final batch suffix byte + emitter.instruction("or cl, 0x20"); // ASCII case-fold it + emitter.instruction("cmp cl, 0x74"); // `.bat` ends in t + emitter.instruction("jne __rt_win_proc_command_uses_cmd_done"); // not `.bat` + emitter.label("__rt_win_proc_command_uses_cmd_yes"); + emitter.instruction("mov eax, 1"); // batch files execute through cmd.exe + emitter.label("__rt_win_proc_command_uses_cmd_done"); + emitter.instruction("ret"); // return the bounded detection result +} + +#[cfg(test)] +mod tests { + //! Purpose: + //! Structural tests for computed Windows `proc_open` argument marshalling. + //! + //! Called from: + //! - `cargo test` through Rust's test harness. + //! + //! Key details: + //! - These tests guard validation, Unicode de-duplication, overflow, and + //! ownership paths before the PE execution tests run under Wine. + + use std::collections::HashSet; + + use super::*; + use crate::codegen_support::platform::Target; + + /// Emits the complete Windows marshalling section for structural assertions. + fn windows_marshalling_assembly() -> String { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_proc_open_marshalling(&mut emitter); + emitter.output().to_string() + } + + /// Verifies every emitted marshalling label is unique, preventing assembler + /// regressions when a helper branch and public entry point have similar names. + #[test] + fn windows_proc_marshalling_labels_are_unique() { + let assembly = windows_marshalling_assembly(); + let mut labels = HashSet::new(); + for line in assembly.lines().filter(|line| line.ends_with(':')) { + assert!(labels.insert(line), "duplicate runtime label: {line}"); + } + } + + /// Verifies dynamic argv quoting includes embedded-quote and trailing-slash + /// expansion plus explicit overflow/invalid-element failure paths. + #[test] + fn windows_proc_command_marshaller_is_checked_and_owned() { + let assembly = windows_marshalling_assembly(); + assert!(assembly.contains("__rt_win_proc_command_array:")); + assert!(assembly.contains("__rt_win_proc_command_uses_cmd:")); + assert!(assembly.contains("__rt_win_proc_command_special_scan")); + assert!(assembly.contains("__rt_win_proc_command_write_caret")); + assert!(assembly.contains("cmp BYTE PTR [r8], 0x2e")); + assert!(assembly.contains("shl r10, 1")); + assert!(assembly.contains("__rt_win_proc_command_write_trailing_slashes")); + assert!(assembly.contains("xchg rdi, rsi")); + assert!(assembly.contains("mov rsi, rdi")); + assert!(assembly.contains("call __rt_heap_alloc")); + assert!(assembly.contains("[rip + __rt_errno], 12")); + assert!(assembly.contains("[rip + __rt_errno], 22")); + } + + /// Verifies environment marshalling uses Windows Unicode case-insensitive + /// comparison, scalar conversion, last-wins replacement, and balanced frees. + #[test] + fn windows_proc_environment_marshaller_deduplicates_and_cleans_up() { + let assembly = windows_marshalling_assembly(); + assert!(assembly.contains("call CompareStringOrdinal")); + assert!(assembly.contains("__rt_win_proc_environment_replace")); + assert!(assembly.contains("call __rt_win_proc_scalar_string")); + assert!(assembly.contains("__rt_win_proc_environment_write_final_nul")); + assert!(assembly.matches("call __rt_heap_free").count() >= 6); + } + + /// Verifies options inspect every documented php-src key, ignore unknown + /// keys, and support boxed Mixed scalar values. + #[test] + fn windows_proc_options_are_runtime_validated() { + let assembly = windows_marshalling_assembly(); + assert!(assembly.contains("call __rt_hash_get")); + assert!(assembly.contains("call __rt_mixed_unbox")); + assert!(assembly.contains("__rt_win_proc_options_suppress_errors_enabled")); + assert!(assembly.contains("__rt_win_proc_options_create_process_group_enabled")); + assert!(assembly.contains("__rt_win_proc_options_create_new_console_enabled")); + assert!(!assembly.contains("__rt_win_proc_options_invalid")); + } +} diff --git a/src/codegen_support/runtime/io/proc_pipe_registry.rs b/src/codegen_support/runtime/io/proc_pipe_registry.rs new file mode 100644 index 0000000000..21a104c9d9 --- /dev/null +++ b/src/codegen_support/runtime/io/proc_pipe_registry.rs @@ -0,0 +1,366 @@ +//! Purpose: +//! Emits the process-to-pipe ownership registry used by `proc_close()`. +//! +//! Called from: +//! - `__rt_proc_open`'s EIR lowerer after it has published `$pipes`, and from +//! `__rt_proc_close` immediately before the platform wait primitive. +//! +//! Key details: +//! - A registry node retains the exact `$pipes` container, so close-time lookup +//! reaches the original kind-1 resource boxes even after a PHP COW copy. +//! - Every selected pipe box receives the `-1` release sentinel before its fd is +//! closed; later scope cleanup therefore cannot close a reused descriptor. +//! - Nodes are raw heap allocations and are removed before the process wait. +//! `__rt_heap_alloc` terminates through `__rt_heap_exhausted` on failure, so a +//! successful `proc_open` result can never escape without a registry node. + +use crate::codegen_support::{abi, emit::Emitter, platform::{Arch, Platform}}; + +/// Emits the register and pre-wait pipe-close helpers for every supported ABI. +pub fn emit_proc_pipe_registry(emitter: &mut Emitter) { + match emitter.target.arch { + Arch::AArch64 => emit_proc_pipe_registry_aarch64(emitter), + Arch::X86_64 => emit_proc_pipe_registry_x86_64(emitter), + } +} + +/// Emits AArch64 registry helpers shared by macOS and Linux. +/// +/// Nodes are `[next, process, retained_pipes]`. The register helper takes +/// `(x0=process, x1=pipes)` and returns the same pair; the close helper takes +/// and returns `x0=process`, making it transparent to `__rt_proc_close`. +fn emit_proc_pipe_registry_aarch64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: proc pipe registry ---"); + emitter.label_global("__rt_proc_pipe_registry_register"); + + // -- retain a process' published pipe container in a raw registry node -- + emitter.instruction("cmn x0, #1"); // proc_open failure sentinel has no pipes to register + emitter.instruction("b.eq __rt_proc_pipe_registry_register_done"); // preserve the failure pair unchanged + emitter.instruction("sub sp, sp, #48"); // reserve stable process, pipes, and node slots + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address + emitter.instruction("mov x29, sp"); // establish the registry helper frame + emitter.instruction("stp x0, x1, [sp, #0]"); // preserve process and pipes across ownership calls + emitter.instruction("mov x0, x1"); // pass pipes to the generic incref helper + abi::emit_call_label(emitter, "__rt_incref"); + emitter.instruction("mov x0, #24"); // three machine words form one raw registry node + abi::emit_call_label(emitter, "__rt_heap_alloc"); + emitter.instruction("str x0, [sp, #16]"); // retain the node pointer across global publication + abi::emit_symbol_address(emitter, "x9", "_proc_pipe_registry_head"); + emitter.instruction("ldr x10, [x9]"); // load the old registry head + emitter.instruction("str x10, [x0]"); // prepend this node to the linked registry + emitter.instruction("ldr x10, [sp, #0]"); // reload the raw process descriptor + emitter.instruction("str x10, [x0, #8]"); // node.process = raw pid or process HANDLE + emitter.instruction("ldr x10, [sp, #8]"); // reload the published pipes container + emitter.instruction("str x10, [x0, #16]"); // node owns one container reference + emitter.instruction("str x0, [x9]"); // publish the new registry head + emitter.instruction("ldp x0, x1, [sp, #0]"); // return the unchanged proc_open result pair + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the registry frame + emitter.instruction("ret"); // return process and pipes to the lowerer + emitter.label("__rt_proc_pipe_registry_register_done"); + emitter.instruction("ret"); // leave proc_open failure untouched + + emitter.label_global("__rt_proc_pipe_registry_close"); + // -- find and unlink the node for this exact process resource -- + emitter.instruction("sub sp, sp, #112"); // reserve registry traversal and iterator state + emitter.instruction("stp x29, x30, [sp, #96]"); // save frame pointer and return address + emitter.instruction("mov x29, sp"); // establish the close helper frame + emitter.instruction("str x0, [sp, #0]"); // save the raw process descriptor for return/wait + abi::emit_symbol_address(emitter, "x9", "_proc_pipe_registry_head"); + emitter.instruction("ldr x10, [x9]"); // start at the registry head + emitter.instruction("str xzr, [sp, #8]"); // previous node = null + emitter.label("__rt_proc_pipe_registry_find"); + emitter.instruction("cbz x10, __rt_proc_pipe_registry_done"); // no node means no pipe ownership was recorded + emitter.instruction("ldr x11, [x10, #8]"); // load this node's process descriptor + emitter.instruction("ldr x12, [sp, #0]"); // reload the requested process descriptor + emitter.instruction("cmp x11, x12"); // is this the matching process node? + emitter.instruction("b.eq __rt_proc_pipe_registry_found"); // unlink and drain this node + emitter.instruction("str x10, [sp, #8]"); // advance previous node before walking onward + emitter.instruction("ldr x10, [x10]"); // follow the next linked registry node + emitter.instruction("b __rt_proc_pipe_registry_find"); // continue the process lookup + + emitter.label("__rt_proc_pipe_registry_found"); + emitter.instruction("str x10, [sp, #16]"); // retain the matched raw node + emitter.instruction("ldr x11, [x10]"); // load the matched node's successor + emitter.instruction("ldr x12, [sp, #8]"); // reload the previous node pointer + emitter.instruction("cbnz x12, __rt_proc_pipe_registry_link_prev"); // non-head nodes relink through their predecessor + abi::emit_symbol_address(emitter, "x9", "_proc_pipe_registry_head"); + emitter.instruction("str x11, [x9]"); // remove the head node from the global list + emitter.instruction("b __rt_proc_pipe_registry_linked"); // continue with the retained container + emitter.label("__rt_proc_pipe_registry_link_prev"); + emitter.instruction("str x11, [x12]"); // bypass the matched node in the linked list + emitter.label("__rt_proc_pipe_registry_linked"); + emitter.instruction("ldr x9, [x10, #16]"); // load the registry-owned pipes container + emitter.instruction("str x9, [sp, #24]"); // preserve pipes through iterator/accessor calls + emitter.instruction("ldr x10, [x9, #-8]"); // inspect the indexed/hash storage kind + emitter.instruction("and x10, x10, #0xff"); // isolate the low-byte container kind + emitter.instruction("str x10, [sp, #48]"); // retain kind: 2 indexed, 3 associative hash + emitter.instruction("ldr x10, [x9]"); // load the number of published pipe entries + emitter.instruction("str x10, [sp, #40]"); // retain total entry count + emitter.instruction("str xzr, [sp, #32]"); // processed entries = 0 + emitter.instruction("str xzr, [sp, #56]"); // hash insertion-order cursor = 0 + emitter.label("__rt_proc_pipe_registry_loop"); + emitter.instruction("ldr x9, [sp, #32]"); // reload processed-entry count + emitter.instruction("ldr x10, [sp, #40]"); // reload container entry count + emitter.instruction("cmp x9, x10"); // have all published pipe entries been visited? + emitter.instruction("b.ge __rt_proc_pipe_registry_release"); // release the retained container once draining is complete + emitter.instruction("ldr x10, [sp, #48]"); // reload the storage kind + emitter.instruction("cmp x10, #3"); // does the container use hash storage? + emitter.instruction("b.eq __rt_proc_pipe_registry_hash_key"); // hashes require insertion-order key iteration + emitter.instruction("str x9, [sp, #64]"); // indexed key is the current zero-based entry index + emitter.instruction("mov x10, #-1"); // integer-key high-word sentinel + emitter.instruction("str x10, [sp, #72]"); // preserve the indexed key representation + emitter.instruction("add x9, x9, #1"); // advance indexed position before the accessor call + emitter.instruction("str x9, [sp, #32]"); // persist processed-entry count + emitter.instruction("b __rt_proc_pipe_registry_get"); // fetch the exact resource box by its key + emitter.label("__rt_proc_pipe_registry_hash_key"); + emitter.instruction("ldr x0, [sp, #24]"); // pass the retained hash container + emitter.instruction("ldr x1, [sp, #56]"); // pass the prior insertion-order cursor + abi::emit_call_label(emitter, "__rt_hash_iter_next"); + emitter.instruction("str x0, [sp, #56]"); // persist the next hash cursor + emitter.instruction("str x1, [sp, #64]"); // retain integer/string key low payload + emitter.instruction("str x2, [sp, #72]"); // retain key high payload or integer sentinel + emitter.instruction("ldr x9, [sp, #32]"); // reload processed-entry count + emitter.instruction("add x9, x9, #1"); // consume this hash entry + emitter.instruction("str x9, [sp, #32]"); // persist processed-entry count + emitter.label("__rt_proc_pipe_registry_get"); + emitter.instruction("ldr x0, [sp, #24]"); // pass the retained pipes container to the generic accessor + emitter.instruction("ldr x1, [sp, #64]"); // pass the original descriptor key low payload + emitter.instruction("ldr x2, [sp, #72]"); // pass its key high payload/sentinel + emitter.instruction("mov x3, #0"); // suppress missing-key diagnostics during teardown + abi::emit_call_label(emitter, "__rt_array_get_mixed_key"); + emitter.instruction("str x0, [sp, #80]"); // retain the accessor-owned result box + emitter.instruction("cbz x0, __rt_proc_pipe_registry_loop"); // a malformed/missing entry needs no close action + emitter.instruction("ldr x9, [x0]"); // inspect the boxed runtime value tag + emitter.instruction("cmp x9, #9"); // is this a resource value? + emitter.instruction("b.ne __rt_proc_pipe_registry_drop_box"); // only resources can own process pipes + emitter.instruction("ldr x9, [x0, #16]"); // inspect the resource subtype + emitter.instruction("cmp x9, #1"); // kind 1 denotes a native stream descriptor + emitter.instruction("b.ne __rt_proc_pipe_registry_drop_box"); // leave non-pipe resources untouched + emitter.instruction("ldr x9, [x0, #8]"); // load the raw pipe descriptor + emitter.instruction("mov x10, #0x40000000"); // sentinel/synthetic descriptor threshold + emitter.instruction("cmp x9, x10"); // has user code already closed this exact resource? + emitter.instruction("b.hs __rt_proc_pipe_registry_drop_box"); // skip sentinel and synthetic stream values + emitter.instruction("mov x10, #-1"); // explicit-release sentinel prevents later double close + emitter.instruction("str x10, [x0, #8]"); // mark the exact shared resource box closed + emitter.instruction("mov x0, x9"); // pass the raw descriptor to the platform close shim + emit_proc_pipe_close_aarch64(emitter); + emitter.label("__rt_proc_pipe_registry_drop_box"); + emitter.instruction("ldr x0, [sp, #80]"); // reload the accessor-owned mixed result + abi::emit_call_label(emitter, "__rt_decref_mixed"); + emitter.instruction("b __rt_proc_pipe_registry_loop"); // continue closing every registered pipe resource + + emitter.label("__rt_proc_pipe_registry_release"); + emitter.instruction("ldr x0, [sp, #24]"); // release the registry's retained pipes container + abi::emit_call_label(emitter, "__rt_decref_any"); + emitter.instruction("ldr x0, [sp, #16]"); // release the raw linked-list node + abi::emit_call_label(emitter, "__rt_heap_free"); + emitter.label("__rt_proc_pipe_registry_done"); + emitter.instruction("ldr x0, [sp, #0]"); // return the unchanged process descriptor to proc_close + emitter.instruction("ldp x29, x30, [sp, #96]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #112"); // release the close helper frame + emitter.instruction("ret"); // continue into the platform process wait +} + +/// Emits the x86_64 registry helpers shared by Linux and Windows. +/// +/// Internal runtime helpers use the established SysV-style convention on both +/// x86_64 targets. Register input/output is `(rdi,rsi)` / `(rax,rdx)` and close +/// input/output is `rdi` / `rax`. +fn emit_proc_pipe_registry_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: proc pipe registry ---"); + emitter.label_global("__rt_proc_pipe_registry_register"); + emitter.instruction("cmp rdi, -1"); // proc_open failure sentinel has no pipes to register + emitter.instruction("je __rt_proc_pipe_registry_register_done_x86"); // preserve the failure pair unchanged + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish the registry helper frame + emitter.instruction("sub rsp, 32"); // reserve process, pipes, and node spill slots + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the raw process descriptor + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // preserve the published pipes container + emitter.instruction("mov rax, rsi"); // pass pipes to the generic incref helper + abi::emit_call_label(emitter, "__rt_incref"); + emitter.instruction("mov rax, 24"); // three machine words form one raw registry node + abi::emit_call_label(emitter, "__rt_heap_alloc"); + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // retain the allocated node pointer + abi::emit_symbol_address(emitter, "r10", "_proc_pipe_registry_head"); + emitter.instruction("mov r11, QWORD PTR [r10]"); // load the old registry head + emitter.instruction("mov QWORD PTR [rax], r11"); // prepend this node to the linked registry + emitter.instruction("mov r11, QWORD PTR [rbp - 8]"); // reload the raw process descriptor + emitter.instruction("mov QWORD PTR [rax + 8], r11"); // node.process = raw pid or process HANDLE + emitter.instruction("mov r11, QWORD PTR [rbp - 16]"); // reload the published pipes container + emitter.instruction("mov QWORD PTR [rax + 16], r11"); // node owns one container reference + emitter.instruction("mov QWORD PTR [r10], rax"); // publish the new registry head + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // return process in the normal result register + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // return pipes in the paired result register + emitter.instruction("add rsp, 32"); // release registry spill storage + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the unchanged proc_open result pair + emitter.label("__rt_proc_pipe_registry_register_done_x86"); + emitter.instruction("mov rax, rdi"); // preserve failure descriptor in the normal result register + emitter.instruction("mov rdx, rsi"); // preserve pipes in the paired result register + emitter.instruction("ret"); // leave proc_open failure untouched + + emitter.label_global("__rt_proc_pipe_registry_close"); + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish the close helper frame + emitter.instruction("sub rsp, 96"); // reserve registry traversal and iterator state + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the raw process descriptor for return/wait + abi::emit_symbol_address(emitter, "r10", "_proc_pipe_registry_head"); + emitter.instruction("mov r11, QWORD PTR [r10]"); // start at the registry head + emitter.instruction("mov QWORD PTR [rbp - 16], 0"); // previous node = null + emitter.label("__rt_proc_pipe_registry_find_x86"); + emitter.instruction("test r11, r11"); // did the registry walk reach its end? + emitter.instruction("jz __rt_proc_pipe_registry_done_x86"); // no node means no pipe ownership was recorded + emitter.instruction("mov rax, QWORD PTR [r11 + 8]"); // load this node's process descriptor + emitter.instruction("cmp rax, QWORD PTR [rbp - 8]"); // is this the matching process node? + emitter.instruction("je __rt_proc_pipe_registry_found_x86"); // unlink and drain this node + emitter.instruction("mov QWORD PTR [rbp - 16], r11"); // advance previous node before walking onward + emitter.instruction("mov r11, QWORD PTR [r11]"); // follow the next linked registry node + emitter.instruction("jmp __rt_proc_pipe_registry_find_x86"); // continue the process lookup + + emitter.label("__rt_proc_pipe_registry_found_x86"); + emitter.instruction("mov QWORD PTR [rbp - 24], r11"); // retain the matched raw node + emitter.instruction("mov rax, QWORD PTR [r11]"); // load the matched node's successor + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the previous node pointer + emitter.instruction("test r10, r10"); // is the matched node the list head? + emitter.instruction("jnz __rt_proc_pipe_registry_link_prev_x86"); // non-head nodes relink through their predecessor + abi::emit_symbol_address(emitter, "r10", "_proc_pipe_registry_head"); + emitter.instruction("mov QWORD PTR [r10], rax"); // remove the head node from the global list + emitter.instruction("jmp __rt_proc_pipe_registry_linked_x86"); // continue with the retained container + emitter.label("__rt_proc_pipe_registry_link_prev_x86"); + emitter.instruction("mov QWORD PTR [r10], rax"); // bypass the matched node in the linked list + emitter.label("__rt_proc_pipe_registry_linked_x86"); + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the matched node + emitter.instruction("mov rax, QWORD PTR [r11 + 16]"); // load the registry-owned pipes container + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // preserve pipes through iterator/accessor calls + emitter.instruction("mov r10, QWORD PTR [rax - 8]"); // inspect the indexed/hash storage kind + emitter.instruction("and r10, 0xff"); // isolate the low-byte container kind + emitter.instruction("mov QWORD PTR [rbp - 56], r10"); // retain kind: 2 indexed, 3 associative hash + emitter.instruction("mov r10, QWORD PTR [rax]"); // load the number of published pipe entries + emitter.instruction("mov QWORD PTR [rbp - 48], r10"); // retain total entry count + emitter.instruction("mov QWORD PTR [rbp - 40], 0"); // processed entries = 0 + emitter.instruction("mov QWORD PTR [rbp - 64], 0"); // hash insertion-order cursor = 0 + emitter.label("__rt_proc_pipe_registry_loop_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload processed-entry count + emitter.instruction("cmp r10, QWORD PTR [rbp - 48]"); // have all published pipe entries been visited? + emitter.instruction("jae __rt_proc_pipe_registry_release_x86"); // release the retained container once draining is complete + emitter.instruction("cmp QWORD PTR [rbp - 56], 3"); // does the container use hash storage? + emitter.instruction("je __rt_proc_pipe_registry_hash_key_x86"); // hashes require insertion-order key iteration + emitter.instruction("mov QWORD PTR [rbp - 72], r10"); // indexed key is the current zero-based entry index + emitter.instruction("mov QWORD PTR [rbp - 80], -1"); // integer-key high-word sentinel + emitter.instruction("inc r10"); // advance indexed position before the accessor call + emitter.instruction("mov QWORD PTR [rbp - 40], r10"); // persist processed-entry count + emitter.instruction("jmp __rt_proc_pipe_registry_get_x86"); // fetch the exact resource box by its key + emitter.label("__rt_proc_pipe_registry_hash_key_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // pass the retained hash container + emitter.instruction("mov rsi, QWORD PTR [rbp - 64]"); // pass the prior insertion-order cursor + abi::emit_call_label(emitter, "__rt_hash_iter_next"); + emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // persist the next hash cursor + emitter.instruction("mov QWORD PTR [rbp - 72], rdi"); // retain integer/string key low payload + emitter.instruction("mov QWORD PTR [rbp - 80], rdx"); // retain key high payload or integer sentinel + emitter.instruction("inc QWORD PTR [rbp - 40]"); // consume this hash entry + emitter.label("__rt_proc_pipe_registry_get_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // pass the retained pipes container to the generic accessor + emitter.instruction("mov rsi, QWORD PTR [rbp - 72]"); // pass the original descriptor key low payload + emitter.instruction("mov rdx, QWORD PTR [rbp - 80]"); // pass its key high payload/sentinel + emitter.instruction("xor ecx, ecx"); // suppress missing-key diagnostics during teardown + abi::emit_call_label(emitter, "__rt_array_get_mixed_key"); + emitter.instruction("mov QWORD PTR [rbp - 88], rax"); // retain the accessor-owned result box + emitter.instruction("test rax, rax"); // did the accessor return a resource box? + emitter.instruction("jz __rt_proc_pipe_registry_loop_x86"); // malformed/missing entries need no close action + emitter.instruction("cmp QWORD PTR [rax], 9"); // is this a resource value? + emitter.instruction("jne __rt_proc_pipe_registry_drop_box_x86"); // only resources can own process pipes + emitter.instruction("cmp QWORD PTR [rax + 16], 1"); // kind 1 denotes a native stream descriptor + emitter.instruction("jne __rt_proc_pipe_registry_drop_box_x86"); // leave non-pipe resources untouched + emitter.instruction("mov rdi, QWORD PTR [rax + 8]"); // load the raw pipe descriptor + if emitter.target.platform == Platform::Windows { + emitter.instruction("cmp rdi, -1"); // has user code already closed this exact resource? + emitter.instruction("je __rt_proc_pipe_registry_drop_box_x86"); // avoid closing the release sentinel twice + } else { + emitter.instruction("cmp rdi, 0x40000000"); // has user code already closed this exact resource? + emitter.instruction("jae __rt_proc_pipe_registry_drop_box_x86"); // skip sentinel and synthetic stream values + } + emitter.instruction("mov QWORD PTR [rax + 8], -1"); // mark the exact shared resource box closed + if emitter.target.platform == Platform::Windows { + abi::emit_call_label(emitter, "__rt_sys_close"); + } else { + emitter.instruction("mov eax, 3"); // Linux x86_64 close syscall number + emitter.instruction("syscall"); // close the descriptor before waiting for the child + } + emitter.label("__rt_proc_pipe_registry_drop_box_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 88]"); // reload the accessor-owned mixed result + abi::emit_call_label(emitter, "__rt_decref_mixed"); + emitter.instruction("jmp __rt_proc_pipe_registry_loop_x86"); // continue closing every registered pipe resource + + emitter.label("__rt_proc_pipe_registry_release_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // release the registry's retained pipes container + abi::emit_call_label(emitter, "__rt_decref_any"); + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // release the raw linked-list node + abi::emit_call_label(emitter, "__rt_heap_free"); + emitter.label("__rt_proc_pipe_registry_done_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // return the unchanged process descriptor to proc_close + emitter.instruction("add rsp, 96"); // release the close helper frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // continue into the platform process wait +} + +#[cfg(test)] +mod tests { + //! Purpose: + //! Structural tests for the process-to-pipe registry runtime helpers. + //! + //! Called from: + //! - `cargo test` through Rust's test harness. + //! + //! Key details: + //! - All targets must retain the pipe container, mark exact kind-1 boxes, + //! close them before wait, and release their registry ownership. + + use super::*; + use crate::codegen_support::platform::{Platform, Target}; + + /// Verifies every supported target emits ownership-safe pipe teardown primitives. + #[test] + fn process_pipe_registry_closes_exact_boxes_on_every_target() { + for target in [ + Target::new(Platform::MacOS, Arch::AArch64), + Target::new(Platform::Linux, Arch::AArch64), + Target::new(Platform::Linux, Arch::X86_64), + Target::new(Platform::Windows, Arch::X86_64), + ] { + let mut emitter = Emitter::new(target); + emit_proc_pipe_registry(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("__rt_proc_pipe_registry_register")); + assert!(asm.contains("__rt_proc_pipe_registry_close")); + assert!(asm.contains("_proc_pipe_registry_head")); + assert!(asm.contains("__rt_array_get_mixed_key")); + assert!(asm.contains("__rt_heap_alloc")); + assert!(!asm.contains("__rt_proc_pipe_registry_register_oom")); + if target.platform == Platform::Windows { + assert!(asm.contains("call __rt_sys_close")); + assert!(asm.contains("cmp rdi, -1")); + } else { + assert!(asm.contains("syscall") || asm.contains("svc")); + } + assert!(asm.contains("__rt_decref_any")); + } + } +} + +/// Emits the direct close operation used by AArch64 registry teardown. +/// +/// macOS and Linux use different syscall numbers, and no generic AArch64 +/// `__rt_sys_close` shim exists. The process registry therefore follows the +/// same direct trap convention as the existing proc_open cleanup path. +fn emit_proc_pipe_close_aarch64(emitter: &mut Emitter) { + if emitter.target.platform == Platform::MacOS { + emitter.instruction("mov x16, #6"); // macOS close syscall number + emitter.instruction("svc #0x80"); // close the descriptor through the macOS trap + } else { + emitter.instruction("mov x8, #57"); // Linux AArch64 close syscall number + emitter.instruction("svc #0"); // close the descriptor through the Linux trap + } +} diff --git a/src/codegen_support/runtime/io/proc_status.rs b/src/codegen_support/runtime/io/proc_status.rs new file mode 100644 index 0000000000..785956cc92 --- /dev/null +++ b/src/codegen_support/runtime/io/proc_status.rs @@ -0,0 +1,851 @@ +//! Purpose: +//! Emits process-status metadata, `proc_get_status`, and `proc_terminate` helpers. +//! +//! Called from: +//! - The process builtin lowerers after `proc_open`, before `proc_close`, and for +//! direct PHP status/termination calls. +//! +//! Key details: +//! - Windows keeps a separate raw registry mapping a live process HANDLE to the +//! persisted command string required by PHP's status result. +//! - The Windows status query never waits or closes the HANDLE; `proc_close` +//! remains the sole consuming operation and unregisters metadata first. + +use crate::codegen_support::{abi, emit::Emitter, platform::{Arch, Platform}}; + +/// Emits process-status helpers for the selected target. +pub(crate) fn emit_proc_status(emitter: &mut Emitter) { + emit_proc_terminate(emitter); + match emitter.target.arch { + Arch::AArch64 => { + emit_registry_aarch64(emitter); + emit_registry_cached_exit_aarch64(emitter); + emit_proc_get_status_unix_aarch64(emitter); + } + Arch::X86_64 => { + emit_registry_x86_64(emitter); + if emitter.target.platform == Platform::Windows { + emit_registry_register_cstr_windows(emitter); + emit_proc_get_status_windows(emitter); + } else { + emit_registry_cached_exit_x86_64(emitter); + emit_proc_get_status_unix_x86_64(emitter); + } + } + } +} + +/// Looks up a normal-exit status harvested by `proc_get_status` on AArch64. +/// +/// The helper retains the registry node; `proc_close` uses the returned exit +/// code and then unregisters the node. Signal and stopped observations are +/// never cached by php-src, so the flag is set only for `WIFEXITED`. +fn emit_registry_cached_exit_aarch64(emitter: &mut Emitter) { + emitter.label_global("__rt_proc_status_cached_exit"); + abi::emit_symbol_address(emitter, "x9", "_proc_status_registry_head"); + emitter.instruction("ldr x10, [x9]"); // start status-node lookup + emitter.label("__rt_proc_status_cached_exit_find"); + emitter.instruction("cbz x10, __rt_proc_status_cached_exit_none"); // no metadata means no cached normal exit + emitter.instruction("ldr x11, [x10, #8]"); // load node process identifier + emitter.instruction("cmp x11, x0"); // does this node belong to the process being closed? + emitter.instruction("b.eq __rt_proc_status_cached_exit_found"); // inspect the matched node's cached-exit slot + emitter.instruction("ldr x10, [x10]"); // follow the registry linked list + emitter.instruction("b __rt_proc_status_cached_exit_find"); // continue lookup + emitter.label("__rt_proc_status_cached_exit_found"); + emitter.instruction("ldr x1, [x10, #32]"); // return the normal-exit cache flag + emitter.instruction("ldr x2, [x10, #40]"); // return the cached normal exit code + emitter.instruction("ret"); // let proc_close decide whether waiting is still required + emitter.label("__rt_proc_status_cached_exit_none"); + emitter.instruction("mov x1, #0"); // missing metadata cannot supply a cached exit code + emitter.instruction("mov x2, #-1"); // preserve proc_close's ordinary failure sentinel + emitter.instruction("ret"); // fall back to blocking wait4 +} + +/// Looks up a normal-exit status harvested by `proc_get_status` on x86_64. +/// +/// Returns the unchanged process descriptor in `rax`, cache flag in `rdx`, and +/// cached exit code in `rcx`; it is emitted only on Unix, where `proc_close` +/// must not wait a second time after PHP cached a `WIFEXITED` result. +fn emit_registry_cached_exit_x86_64(emitter: &mut Emitter) { + emitter.label_global("__rt_proc_status_cached_exit"); + emitter.instruction("mov rax, rdi"); // preserve the process descriptor for proc_close + abi::emit_symbol_address(emitter, "r10", "_proc_status_registry_head"); + emitter.instruction("mov r11, QWORD PTR [r10]"); // start status-node lookup + emitter.label("__rt_proc_status_cached_exit_find_x86"); + emitter.instruction("test r11, r11"); // reached the end of the metadata list? + emitter.instruction("jz __rt_proc_status_cached_exit_none_x86"); // no node means no cached normal exit + emitter.instruction("cmp QWORD PTR [r11 + 8], rdi"); // does this node belong to the process being closed? + emitter.instruction("je __rt_proc_status_cached_exit_found_x86"); // inspect the matched node cache + emitter.instruction("mov r11, QWORD PTR [r11]"); // follow the registry linked list + emitter.instruction("jmp __rt_proc_status_cached_exit_find_x86"); // continue lookup + emitter.label("__rt_proc_status_cached_exit_found_x86"); + emitter.instruction("mov rdx, QWORD PTR [r11 + 32]"); // return the normal-exit cache flag + emitter.instruction("mov rcx, QWORD PTR [r11 + 40]"); // return the cached normal exit code + emitter.instruction("ret"); // let proc_close decide whether waiting is still required + emitter.label("__rt_proc_status_cached_exit_none_x86"); + emitter.instruction("xor edx, edx"); // missing metadata cannot supply a cached exit code + emitter.instruction("mov rcx, -1"); // preserve proc_close's ordinary failure sentinel + emitter.instruction("ret"); // fall back to blocking wait4 +} + +/// Emits PHP's `proc_terminate` primitive for every supported target. +fn emit_proc_terminate(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: proc_terminate ---"); + emitter.label_global("__rt_proc_terminate"); + match emitter.target.arch { + Arch::AArch64 => { + if emitter.target.platform == Platform::MacOS { + emitter.instruction("mov x16, #37"); // macOS kill syscall number + emitter.instruction("svc #0x80"); // send the requested signal to the child PID + emitter.instruction("cset x0, cc"); // macOS reports syscall failure through the carry flag + } else { + emitter.instruction("mov x8, #129"); // Linux AArch64 kill syscall number + emitter.instruction("svc #0"); // send the requested signal to the child PID + emitter.instruction("cmp x0, #0"); // Linux uses a signed zero-or-negative syscall return + emitter.instruction("cset x0, eq"); // materialize PHP true/false from the syscall result + } + emitter.instruction("ret"); // return without consuming the process resource + } + Arch::X86_64 if emitter.target.platform == Platform::Windows => { + emitter.instruction("sub rsp, 40"); // reserve MSx64 shadow space with call-site alignment + emitter.instruction("mov rcx, rdi"); // TerminateProcess first argument is the process HANDLE + emitter.instruction("mov edx, 255"); // php-src always terminates Windows processes with exit code 255 + emitter.instruction("call TerminateProcess"); // terminate the retained Windows process HANDLE + emitter.instruction("test eax, eax"); // Win32 returns non-zero on success + emitter.instruction("setne al"); // convert Win32 BOOL to the PHP boolean representation + emitter.instruction("movzx eax, al"); // clear high result bits after setne + emitter.instruction("add rsp, 40"); // release MSx64 shadow space + emitter.instruction("ret"); // leave handle ownership with proc_close + } + Arch::X86_64 => { + emitter.instruction("mov eax, 62"); // Linux x86_64 kill syscall number + emitter.instruction("syscall"); // send rsi signal to rdi child PID + emitter.instruction("test rax, rax"); // Linux returns a negative errno on failure + emitter.instruction("setns al"); // true when the syscall result is zero or positive + emitter.instruction("movzx eax, al"); // clear high result bits after setns + emitter.instruction("ret"); // return PHP boolean without reaping the child + } + } +} + +/// Emits the Windows-only metadata registry and status hash builder. +/// +/// A registry node is eight words: `next`, `HANDLE`, persisted command pointer +/// and length, plus four reserved status words. The reserved words make the +/// Unix cached-wait extension ABI-compatible without changing the Windows node +/// allocation shape; Windows itself deliberately reports `cached => false`. +fn emit_registry_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: proc status registry ---"); + emit_registry_register_windows(emitter); + emit_registry_unregister_windows(emitter); +} + +/// Emits the AArch64 status registry shared by macOS and Linux. +/// +/// Nodes have the same eight-word layout as x86_64: next, process PID, +/// persisted command pointer/length, then cached-status slots used by Unix +/// `wait4(WNOHANG|WUNTRACED)` queries. +fn emit_registry_aarch64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: proc status registry ---"); + emitter.label_global("__rt_proc_status_register"); + emitter.instruction("cmn x0, #1"); // failed proc_open results have no process metadata + emitter.instruction("b.eq __rt_proc_status_register_done"); // leave failure untouched without allocating + emitter.instruction("sub sp, sp, #64"); // reserve process, command, and node spill slots + emitter.instruction("stp x29, x30, [sp, #48]"); // preserve frame pointer and return address + emitter.instruction("mov x29, sp"); // establish the registry helper frame + emitter.instruction("str x0, [sp, #0]"); // retain the raw child PID + emitter.instruction("str x1, [sp, #8]"); // retain command pointer across persistence + emitter.instruction("str x2, [sp, #16]"); // retain command byte length across persistence + abi::emit_call_label(emitter, "__rt_str_persist"); // copy command bytes into registry-owned storage + emitter.instruction("cbz x1, __rt_proc_status_register_oom"); // a null persisted pointer cannot back status records + emitter.instruction("str x1, [sp, #24]"); // retain owned command pointer + emitter.instruction("str x2, [sp, #32]"); // retain owned command length + emitter.instruction("mov x0, #64"); // allocate the fixed eight-word metadata node + abi::emit_call_label(emitter, "__rt_heap_alloc"); // reserve raw registry storage + emitter.instruction("cbz x0, __rt_proc_status_register_node_oom"); // release command copy when node allocation failed + abi::emit_symbol_address(emitter, "x9", "_proc_status_registry_head"); + emitter.instruction("ldr x10, [x9]"); // load old metadata head + emitter.instruction("str x10, [x0]"); // node.next = old head + emitter.instruction("ldr x10, [sp, #0]"); // reload raw child PID + emitter.instruction("str x10, [x0, #8]"); // node.process = child PID + emitter.instruction("ldr x10, [sp, #24]"); // reload owned command pointer + emitter.instruction("str x10, [x0, #16]"); // node.command_ptr = owned bytes + emitter.instruction("ldr x10, [sp, #32]"); // reload owned command length + emitter.instruction("str x10, [x0, #24]"); // node.command_len = byte length + emitter.instruction("str xzr, [x0, #32]"); // clear cached-exit flag + emitter.instruction("str xzr, [x0, #40]"); // clear cached exit code + emitter.instruction("str xzr, [x0, #48]"); // clear cached termination signal + emitter.instruction("str xzr, [x0, #56]"); // clear cached stop signal + emitter.instruction("str x0, [x9]"); // publish fully initialized metadata node + emitter.instruction("ldr x0, [sp, #0]"); // return original process PID + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // release registry frame + emitter.instruction("ret"); // resume proc_open lowering + emitter.label("__rt_proc_status_register_node_oom"); + emitter.instruction("ldr x0, [sp, #24]"); // load orphaned command copy + abi::emit_call_label(emitter, "__rt_decref_any"); // release ownership before falling back + emitter.label("__rt_proc_status_register_oom"); + emitter.instruction("ldr x0, [sp, #0]"); // preserve successful proc_open result + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // release registry frame + emitter.instruction("ret"); // no metadata is preferable to failing proc_open + emitter.label("__rt_proc_status_register_done"); + emitter.instruction("ret"); // preserve raw failure PID sentinel + + emitter.label_global("__rt_proc_status_unregister"); + emitter.instruction("sub sp, sp, #48"); // reserve process, previous, and matched-node slots + emitter.instruction("stp x29, x30, [sp, #32]"); // preserve frame pointer and return address + emitter.instruction("mov x29, sp"); // establish the unlink helper frame + emitter.instruction("str x0, [sp, #0]"); // retain the child PID for return + abi::emit_symbol_address(emitter, "x9", "_proc_status_registry_head"); + emitter.instruction("ldr x10, [x9]"); // begin list traversal at the registry head + emitter.instruction("str xzr, [sp, #8]"); // previous node = null + emitter.label("__rt_proc_status_unregister_find"); + emitter.instruction("cbz x10, __rt_proc_status_unregister_done"); // absent metadata is harmless during proc_close + emitter.instruction("ldr x11, [x10, #8]"); // load candidate process PID + emitter.instruction("ldr x12, [sp, #0]"); // reload requested process PID + emitter.instruction("cmp x11, x12"); // does this node match the consumed process? + emitter.instruction("b.eq __rt_proc_status_unregister_found"); // unlink the matching node + emitter.instruction("str x10, [sp, #8]"); // advance predecessor pointer + emitter.instruction("ldr x10, [x10]"); // follow the next metadata node + emitter.instruction("b __rt_proc_status_unregister_find"); // continue the linked-list walk + emitter.label("__rt_proc_status_unregister_found"); + emitter.instruction("str x10, [sp, #16]"); // retain matched node across cleanup calls + emitter.instruction("ldr x11, [x10]"); // load successor before unlinking + emitter.instruction("ldr x12, [sp, #8]"); // reload predecessor pointer + emitter.instruction("cbnz x12, __rt_proc_status_unregister_link_prev"); // non-head nodes relink through their predecessor + abi::emit_symbol_address(emitter, "x9", "_proc_status_registry_head"); + emitter.instruction("str x11, [x9]"); // remove the matched head node + emitter.instruction("b __rt_proc_status_unregister_unlinked"); // free detached ownership + emitter.label("__rt_proc_status_unregister_link_prev"); + emitter.instruction("str x11, [x12]"); // bypass the matched node + emitter.label("__rt_proc_status_unregister_unlinked"); + emitter.instruction("ldr x10, [sp, #16]"); // reload detached metadata node + emitter.instruction("ldr x0, [x10, #16]"); // load its owned command string + abi::emit_call_label(emitter, "__rt_decref_any"); // release command ownership + emitter.instruction("ldr x0, [sp, #16]"); // reload detached raw node + abi::emit_call_label(emitter, "__rt_heap_free"); // free registry storage + emitter.label("__rt_proc_status_unregister_done"); + emitter.instruction("ldr x0, [sp, #0]"); // return process PID to proc_close + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release unlink helper frame + emitter.instruction("ret"); // continue into wait4 +} + +/// Emits Unix `proc_get_status` on AArch64 with cached non-blocking wait state. +fn emit_proc_get_status_unix_aarch64(emitter: &mut Emitter) { + let macos = emitter.target.platform == Platform::MacOS; + emitter.blank(); + emitter.comment("--- runtime: Unix proc_get_status ---"); + emitter.label_global("__rt_proc_get_status"); + emitter.instruction("sub sp, sp, #144"); // reserve lookup, wait-status, and status-hash state + emitter.instruction("stp x29, x30, [sp, #128]"); // preserve frame pointer and return address + emitter.instruction("mov x29, sp"); // establish the status-query frame + emitter.instruction("str x0, [sp, #8]"); // retain requested child PID + abi::emit_symbol_address(emitter, "x9", "_proc_status_registry_head"); + emitter.instruction("ldr x10, [x9]"); // start process metadata lookup + emitter.label("__rt_proc_get_status_find"); + emitter.instruction("cbz x10, __rt_proc_get_status_missing"); // closed/unknown process has no PHP status record + emitter.instruction("ldr x11, [x10, #8]"); // load node process PID + emitter.instruction("ldr x12, [sp, #8]"); // reload requested child PID + emitter.instruction("cmp x11, x12"); // does this metadata node match? + emitter.instruction("b.eq __rt_proc_get_status_found"); // retain matching node + emitter.instruction("ldr x10, [x10]"); // follow metadata list + emitter.instruction("b __rt_proc_get_status_find"); // continue process lookup + emitter.label("__rt_proc_get_status_found"); + emitter.instruction("str x10, [sp, #0]"); // retain metadata node across wait/hash calls + emitter.instruction("mov x9, #1"); // default running=true before non-blocking wait + emitter.instruction("str x9, [sp, #24]"); // save running flag + emitter.instruction("str xzr, [sp, #32]"); // default signaled=false + emitter.instruction("str xzr, [sp, #40]"); // default stopped=false + emitter.instruction("mov x9, #-1"); // PHP uses -1 while exit status is unknown + emitter.instruction("str x9, [sp, #48]"); // save default exitcode + emitter.instruction("str xzr, [sp, #56]"); // default termsig=0 + emitter.instruction("str xzr, [sp, #64]"); // default stopsig=0 + emitter.instruction("str xzr, [sp, #72]"); // default cached=false + emitter.instruction("ldr x10, [sp, #0]"); // reload metadata node + emitter.instruction("ldr x9, [x10, #32]"); // inspect cached-exit flag + emitter.instruction("cbz x9, __rt_proc_get_status_wait"); // query wait4 until a terminal status is cached + emitter.instruction("str xzr, [sp, #24]"); // cached terminal state is not running + emitter.instruction("mov x9, #1"); // report PHP cached=true after prior terminal observation + emitter.instruction("str x9, [sp, #72]"); // retain cached flag for hash construction + emitter.instruction("ldr x9, [x10, #40]"); // reload cached exit code + emitter.instruction("str x9, [sp, #48]"); // expose cached exit code + emitter.instruction("str xzr, [sp, #56]"); // only normal exits are cached by php-src + emitter.instruction("b __rt_proc_get_status_hash_unix"); // build the stable cached record + emitter.label("__rt_proc_get_status_wait"); + emitter.instruction("ldr x0, [sp, #8]"); // wait4 first argument = child PID + emitter.instruction("add x1, sp, #16"); // wait4 second argument = raw-status word + emitter.instruction("mov x2, #3"); // WNOHANG | WUNTRACED + emitter.instruction("mov x3, #0"); // no rusage output + if macos { + emitter.instruction("mov x16, #7"); // macOS wait4 syscall number + emitter.instruction("svc #0x80"); // observe child state without blocking + emitter.instruction("b.cs __rt_proc_get_status_no_child"); // Darwin signals wait4 errors through carry, not a negative result + } else { + emitter.instruction("mov x8, #260"); // Linux AArch64 wait4 syscall number + emitter.instruction("svc #0"); // observe child state without blocking + } + emitter.instruction("cmp x0, #0"); // distinguish live child, error, and returned PID + emitter.instruction("b.eq __rt_proc_get_status_hash_unix"); // zero means WNOHANG found the child still running + emitter.instruction("b.lt __rt_proc_get_status_no_child"); // ECHILD/error means process is no longer running + emitter.instruction("ldr x9, [sp, #16]"); // inspect raw wait status for returned child + emitter.instruction("and x10, x9, #0x7f"); // low seven bits classify exit/signal/stop + emitter.instruction("cbz x10, __rt_proc_get_status_exited"); // zero low bits means normal exit + emitter.instruction("cmp x10, #0x7f"); // 0x7f marks WIFSTOPPED + emitter.instruction("b.eq __rt_proc_get_status_stopped"); // stopped children remain running in PHP's record + emitter.instruction("str xzr, [sp, #24]"); // signal termination means no longer running + emitter.instruction("mov x11, #1"); // mark PHP signaled=true + emitter.instruction("str x11, [sp, #32]"); // save signaled flag + emitter.instruction("str x10, [sp, #56]"); // termsig = low status signal number + emitter.instruction("b __rt_proc_get_status_hash_unix"); // signal status is intentionally not cached by php-src + emitter.label("__rt_proc_get_status_exited"); + emitter.instruction("str xzr, [sp, #24]"); // normal exit means no longer running + emitter.instruction("lsr x10, x9, #8"); // move exit status into low byte + emitter.instruction("and x10, x10, #0xff"); // retain PHP's 0..255 exit status + emitter.instruction("str x10, [sp, #48]"); // expose completed exit code + emitter.instruction("ldr x11, [sp, #0]"); // reload metadata node + emitter.instruction("mov x12, #1"); // terminal status becomes cacheable after this first observation + emitter.instruction("str x12, [x11, #32]"); // store cached-exit flag + emitter.instruction("str x10, [x11, #40]"); // cache completed exit code + emitter.instruction("str xzr, [x11, #48]"); // normal exit has no termination signal + emitter.instruction("mov x12, #1"); // PHP reports cached=true on the harvesting observation itself + emitter.instruction("str x12, [sp, #72]"); // expose the successful normal-exit cache state + emitter.instruction("b __rt_proc_get_status_hash_unix"); // build normal-exit status with cached=true + emitter.label("__rt_proc_get_status_stopped"); + emitter.instruction("mov x11, #1"); // PHP reports stopped=true without clearing running + emitter.instruction("str x11, [sp, #40]"); // retain stopped flag + emitter.instruction("lsr x10, x9, #8"); // move stop signal into low byte + emitter.instruction("and x10, x10, #0xff"); // retain stop signal number + emitter.instruction("str x10, [sp, #64]"); // expose stopsig + emitter.instruction("b __rt_proc_get_status_hash_unix"); // stopped state is deliberately not terminal-cached + emitter.label("__rt_proc_get_status_no_child"); + emitter.instruction("str xzr, [sp, #24]"); // an unobservable child cannot remain running + emitter.label("__rt_proc_get_status_hash_unix"); + emit_proc_status_hash_aarch64(emitter); + emitter.instruction("ldp x29, x30, [sp, #128]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #144"); // release status-query frame + emitter.instruction("ret"); // return associative hash pointer + emitter.label("__rt_proc_get_status_missing"); + emitter.instruction("mov x0, #0"); // unknown/closed resource maps to PHP false + emitter.instruction("ldp x29, x30, [sp, #128]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #144"); // release status-query frame + emitter.instruction("ret"); // return null hash pointer +} + +/// Builds the AArch64 Unix status hash from the standard query-frame slots. +fn emit_proc_status_hash_aarch64(emitter: &mut Emitter) { + emitter.instruction("mov x0, #16"); // allocate enough buckets for all PHP status fields + emitter.instruction("mov x1, #7"); // status values use heterogeneous Mixed storage + abi::emit_call_label(emitter, "__rt_hash_new"); // create result associative hash + emitter.instruction("str x0, [sp, #80]"); // retain potentially-growing hash pointer + emitter.instruction("ldr x10, [sp, #0]"); // reload metadata node for command ownership + emitter.instruction("ldr x3, [x10, #16]"); // value low word = registry-owned command pointer + emitter.instruction("ldr x4, [x10, #24]"); // value high word = command byte length + emitter.instruction("mov x5, #1"); // tag 1 = string + emit_proc_status_hash_put_aarch64(emitter, "_proc_status_k_command", 7); + emitter.instruction("ldr x3, [sp, #8]"); // value low word = child PID + emitter.instruction("mov x4, #0"); // integers have no high payload word + emitter.instruction("mov x5, #0"); // tag 0 = integer + emit_proc_status_hash_put_aarch64(emitter, "_proc_status_k_pid", 3); + emit_proc_status_hash_bool_aarch64(emitter, "_proc_status_k_cached", 6, "[sp, #72]"); + emit_proc_status_hash_bool_aarch64(emitter, "_proc_status_k_running", 7, "[sp, #24]"); + emit_proc_status_hash_bool_aarch64(emitter, "_proc_status_k_signaled", 8, "[sp, #32]"); + emit_proc_status_hash_bool_aarch64(emitter, "_proc_status_k_stopped", 7, "[sp, #40]"); + emitter.instruction("ldr x3, [sp, #48]"); // value low word = exitcode or PHP -1 sentinel + emitter.instruction("mov x4, #0"); // integers have no high payload word + emitter.instruction("mov x5, #0"); // tag 0 = integer + emit_proc_status_hash_put_aarch64(emitter, "_proc_status_k_exitcode", 8); + emit_proc_status_hash_int_aarch64(emitter, "_proc_status_k_termsig", 7, "[sp, #56]"); + emit_proc_status_hash_int_aarch64(emitter, "_proc_status_k_stopsig", 7, "[sp, #64]"); + emitter.instruction("ldr x0, [sp, #80]"); // return completed status hash pointer +} + +/// Inserts the staged AArch64 Mixed triple under one static PHP status key. +fn emit_proc_status_hash_put_aarch64(emitter: &mut Emitter, key: &str, len: i64) { + emitter.instruction("mov x0, x5"); // move the concrete value tag into the mixed-box ABI + emitter.instruction("mov x1, x3"); // move the concrete low payload into the mixed-box ABI + emitter.instruction("mov x2, x4"); // move the concrete high payload into the mixed-box ABI + abi::emit_call_label(emitter, "__rt_mixed_from_value"); // give this heterogeneous hash entry its own boxed owner + emitter.instruction("mov x3, x0"); // hash value low word = boxed Mixed cell + emitter.instruction("mov x4, #0"); // boxed Mixed values have no separate high word + emitter.instruction("mov x5, #7"); // entry tag 7 identifies a boxed Mixed cell + abi::emit_symbol_address(emitter, "x1", key); + emitter.instruction(&format!("mov x2, #{len}")); // pass static status-key byte length + emitter.instruction("ldr x0, [sp, #80]"); // reload current possibly-grown hash + abi::emit_call_label(emitter, "__rt_hash_set"); // insert status field + emitter.instruction("str x0, [sp, #80]"); // retain potentially-reallocated hash pointer +} + +/// Inserts one boolean AArch64 status slot. +fn emit_proc_status_hash_bool_aarch64(emitter: &mut Emitter, key: &str, len: i64, slot: &str) { + emitter.instruction(&format!("ldr x3, {slot}")); // stage boolean payload from query frame + emitter.instruction("mov x4, #0"); // booleans have no high payload word + emitter.instruction("mov x5, #3"); // tag 3 = boolean + emit_proc_status_hash_put_aarch64(emitter, key, len); +} + +/// Inserts one integer AArch64 status slot. +fn emit_proc_status_hash_int_aarch64(emitter: &mut Emitter, key: &str, len: i64, slot: &str) { + emitter.instruction(&format!("ldr x3, {slot}")); // stage integer payload from query frame + emitter.instruction("mov x4, #0"); // integers have no high payload word + emitter.instruction("mov x5, #0"); // tag 0 = integer + emit_proc_status_hash_put_aarch64(emitter, key, len); +} + +/// Emits Linux-x86_64 `proc_get_status` with cached non-blocking wait state. +fn emit_proc_get_status_unix_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: Unix proc_get_status ---"); + emitter.label_global("__rt_proc_get_status"); + emitter.instruction("push rbp"); // preserve caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish status-query frame + emitter.instruction("sub rsp, 128"); // reserve lookup, wait status, and hash state + emitter.instruction("mov QWORD PTR [rbp - 16], rdi"); // retain requested child PID + abi::emit_symbol_address(emitter, "r10", "_proc_status_registry_head"); + emitter.instruction("mov r11, QWORD PTR [r10]"); // start process metadata lookup + emitter.label("__rt_proc_get_status_find"); + emitter.instruction("test r11, r11"); // reached the end of the metadata list? + emitter.instruction("jz __rt_proc_get_status_missing"); // closed/unknown process has no PHP status record + emitter.instruction("cmp QWORD PTR [r11 + 8], rdi"); // does this metadata node match the requested PID? + emitter.instruction("je __rt_proc_get_status_found"); // retain the matching node + emitter.instruction("mov r11, QWORD PTR [r11]"); // follow the next metadata node + emitter.instruction("jmp __rt_proc_get_status_find"); // continue list traversal + emitter.label("__rt_proc_get_status_found"); + emitter.instruction("mov QWORD PTR [rbp - 8], r11"); // retain metadata node across wait/hash calls + emitter.instruction("mov QWORD PTR [rbp - 32], 1"); // default running=true before non-blocking wait + emitter.instruction("mov QWORD PTR [rbp - 40], 0"); // default signaled=false + emitter.instruction("mov QWORD PTR [rbp - 48], 0"); // default stopped=false + emitter.instruction("mov QWORD PTR [rbp - 56], -1"); // PHP exitcode=-1 while status is unknown + emitter.instruction("mov QWORD PTR [rbp - 64], 0"); // default termsig=0 + emitter.instruction("mov QWORD PTR [rbp - 72], 0"); // default stopsig=0 + emitter.instruction("mov QWORD PTR [rbp - 80], 0"); // default cached=false + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload metadata node + emitter.instruction("cmp QWORD PTR [r10 + 32], 0"); // was a terminal wait status cached already? + emitter.instruction("je __rt_proc_get_status_wait_x86"); // query wait4 until a terminal observation exists + emitter.instruction("mov QWORD PTR [rbp - 32], 0"); // cached terminal state is no longer running + emitter.instruction("mov QWORD PTR [rbp - 80], 1"); // subsequent status calls report cached=true + emitter.instruction("mov rax, QWORD PTR [r10 + 40]"); // reload cached exit code + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // expose cached exit code + emitter.instruction("mov QWORD PTR [rbp - 64], 0"); // only normal exits are cached by php-src + emitter.instruction("jmp __rt_proc_get_status_hash_unix_x86"); // build stable cached status record + emitter.label("__rt_proc_get_status_wait_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // wait4 first argument = child PID + emitter.instruction("lea rsi, [rbp - 24]"); // wait4 second argument = raw status word + emitter.instruction("mov edx, 3"); // WNOHANG | WUNTRACED + emitter.instruction("xor r10d, r10d"); // no rusage output + emitter.instruction("mov eax, 61"); // Linux x86_64 wait4 syscall number + emitter.instruction("syscall"); // observe child state without blocking + emitter.instruction("test rax, rax"); // distinguish live child, error, and returned PID + emitter.instruction("jz __rt_proc_get_status_hash_unix_x86"); // zero means WNOHANG found child still running + emitter.instruction("js __rt_proc_get_status_no_child_x86"); // ECHILD/error means no running child remains + emitter.instruction("mov r9, QWORD PTR [rbp - 24]"); // inspect raw wait status for returned child + emitter.instruction("mov r10, r9"); // copy status for low-bit classification + emitter.instruction("and r10, 0x7f"); // low seven bits classify exit/signal/stop + emitter.instruction("jz __rt_proc_get_status_exited_x86"); // zero low bits means normal exit + emitter.instruction("cmp r10, 0x7f"); // 0x7f marks WIFSTOPPED + emitter.instruction("je __rt_proc_get_status_stopped_x86"); // stopped children remain running in PHP's record + emitter.instruction("mov QWORD PTR [rbp - 32], 0"); // signal termination means no longer running + emitter.instruction("mov QWORD PTR [rbp - 40], 1"); // mark PHP signaled=true + emitter.instruction("mov QWORD PTR [rbp - 64], r10"); // termsig = low status signal number + emitter.instruction("jmp __rt_proc_get_status_hash_unix_x86"); // signal status is intentionally not cached by php-src + emitter.label("__rt_proc_get_status_exited_x86"); + emitter.instruction("mov QWORD PTR [rbp - 32], 0"); // normal exit means no longer running + emitter.instruction("shr r9, 8"); // move exit status into low byte + emitter.instruction("and r9, 0xff"); // retain PHP's 0..255 exit status + emitter.instruction("mov QWORD PTR [rbp - 56], r9"); // expose completed exit code + emitter.instruction("mov r11, QWORD PTR [rbp - 8]"); // reload metadata node + emitter.instruction("mov QWORD PTR [r11 + 32], 1"); // cache terminal status after this first observation + emitter.instruction("mov QWORD PTR [r11 + 40], r9"); // cache completed exit code + emitter.instruction("mov QWORD PTR [r11 + 48], 0"); // normal exit has no termination signal + emitter.instruction("mov QWORD PTR [rbp - 80], 1"); // PHP reports cached=true on the harvesting observation itself + emitter.instruction("jmp __rt_proc_get_status_hash_unix_x86"); // build normal-exit status with cached=true + emitter.label("__rt_proc_get_status_stopped_x86"); + emitter.instruction("mov QWORD PTR [rbp - 48], 1"); // PHP reports stopped=true without clearing running + emitter.instruction("shr r9, 8"); // move stop signal into low byte + emitter.instruction("and r9, 0xff"); // retain stop signal number + emitter.instruction("mov QWORD PTR [rbp - 72], r9"); // expose stopsig + emitter.instruction("jmp __rt_proc_get_status_hash_unix_x86"); // stopped state is not terminal-cached + emitter.label("__rt_proc_get_status_no_child_x86"); + emitter.instruction("mov QWORD PTR [rbp - 32], 0"); // an unobservable child cannot remain running + emitter.label("__rt_proc_get_status_hash_unix_x86"); + emit_proc_status_hash_x86_64(emitter); + emitter.instruction("add rsp, 128"); // release status-query frame + emitter.instruction("pop rbp"); // restore caller frame pointer + emitter.instruction("ret"); // return associative hash pointer + emitter.label("__rt_proc_get_status_missing"); + emitter.instruction("xor eax, eax"); // unknown/closed resource maps to PHP false + emitter.instruction("add rsp, 128"); // release status-query frame + emitter.instruction("pop rbp"); // restore caller frame pointer + emitter.instruction("ret"); // return null hash pointer +} + +/// Builds the x86_64 Unix status hash from the standard query-frame slots. +fn emit_proc_status_hash_x86_64(emitter: &mut Emitter) { + emitter.instruction("mov rdi, 16"); // allocate enough buckets for all PHP status fields + emitter.instruction("mov rsi, 7"); // status values use heterogeneous Mixed storage + emitter.instruction("call __rt_hash_new"); // create result associative hash + emitter.instruction("mov QWORD PTR [rbp - 88], rax"); // retain potentially-growing hash pointer + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload metadata node for command ownership + emitter.instruction("mov rcx, QWORD PTR [r10 + 16]"); // value low word = registry-owned command pointer + emitter.instruction("mov r8, QWORD PTR [r10 + 24]"); // value high word = command byte length + emitter.instruction("mov r9, 1"); // tag 1 = string + emit_proc_status_hash_put_x86_64(emitter, "_proc_status_k_command", 7); + emitter.instruction("mov rcx, QWORD PTR [rbp - 16]"); // value low word = child PID + emitter.instruction("xor r8d, r8d"); // integers have no high payload word + emitter.instruction("xor r9d, r9d"); // tag 0 = integer + emit_proc_status_hash_put_x86_64(emitter, "_proc_status_k_pid", 3); + emit_proc_status_hash_bool_x86_64(emitter, "_proc_status_k_cached", 6, 80); + emit_proc_status_hash_bool_x86_64(emitter, "_proc_status_k_running", 7, 32); + emit_proc_status_hash_bool_x86_64(emitter, "_proc_status_k_signaled", 8, 40); + emit_proc_status_hash_bool_x86_64(emitter, "_proc_status_k_stopped", 7, 48); + emitter.instruction("mov rcx, QWORD PTR [rbp - 56]"); // value low word = exitcode or PHP -1 sentinel + emitter.instruction("xor r8d, r8d"); // integers have no high payload word + emitter.instruction("xor r9d, r9d"); // tag 0 = integer + emit_proc_status_hash_put_x86_64(emitter, "_proc_status_k_exitcode", 8); + emit_proc_status_hash_int_x86_64(emitter, "_proc_status_k_termsig", 7, 64); + emit_proc_status_hash_int_x86_64(emitter, "_proc_status_k_stopsig", 7, 72); + emitter.instruction("mov rax, QWORD PTR [rbp - 88]"); // return completed status hash pointer +} + +/// Inserts the staged x86_64 Mixed triple under one static PHP status key. +fn emit_proc_status_hash_put_x86_64(emitter: &mut Emitter, key: &str, len: i64) { + emitter.instruction("mov rax, r9"); // move the concrete value tag into the mixed-box ABI + emitter.instruction("mov rdi, rcx"); // move the concrete low payload into the mixed-box ABI + emitter.instruction("mov rsi, r8"); // move the concrete high payload into the mixed-box ABI + emitter.instruction("call __rt_mixed_from_value"); // give this heterogeneous hash entry its own boxed owner + emitter.instruction("mov rcx, rax"); // hash value low word = boxed Mixed cell + emitter.instruction("xor r8d, r8d"); // boxed Mixed values have no separate high word + emitter.instruction("mov r9, 7"); // entry tag 7 identifies a boxed Mixed cell + abi::emit_symbol_address(emitter, "rsi", key); + emitter.instruction(&format!("mov rdx, {len}")); // pass static status-key byte length + emitter.instruction("mov rdi, QWORD PTR [rbp - 88]"); // reload current possibly-grown hash + emitter.instruction("call __rt_hash_set"); // insert status field + emitter.instruction("mov QWORD PTR [rbp - 88], rax"); // retain potentially-reallocated hash pointer +} + +/// Inserts one boolean x86_64 status slot. +fn emit_proc_status_hash_bool_x86_64(emitter: &mut Emitter, key: &str, len: i64, slot: i64) { + emitter.instruction(&format!("mov rcx, QWORD PTR [rbp - {slot}]")); // stage boolean payload from query frame + emitter.instruction("xor r8d, r8d"); // booleans have no high payload word + emitter.instruction("mov r9, 3"); // tag 3 = boolean + emit_proc_status_hash_put_x86_64(emitter, key, len); +} + +/// Inserts one integer x86_64 status slot. +fn emit_proc_status_hash_int_x86_64(emitter: &mut Emitter, key: &str, len: i64, slot: i64) { + emitter.instruction(&format!("mov rcx, QWORD PTR [rbp - {slot}]")); // stage integer payload from query frame + emitter.instruction("xor r8d, r8d"); // integers have no high payload word + emitter.instruction("xor r9d, r9d"); // tag 0 = integer + emit_proc_status_hash_put_x86_64(emitter, key, len); +} + +/// Persists the command associated with a successfully opened Windows process. +/// +/// Inputs follow the internal SysV convention: `rdi = HANDLE`, `rsi = command` +/// and `rdx = command length`. The raw process result is returned in `rax` so +/// the helper can be inserted transparently between `proc_open` and boxing. +fn emit_registry_register_windows(emitter: &mut Emitter) { + emitter.label_global("__rt_proc_status_register"); + emitter.instruction("cmp rdi, -1"); // failed proc_open results have no status metadata + emitter.instruction("je __rt_proc_status_register_done"); // preserve failure without allocating registry state + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable registry frame + emitter.instruction("sub rsp, 48"); // reserve process, command, and node spill slots + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the raw process HANDLE across allocations + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // preserve command bytes until they are copied + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // preserve the exact command byte length + emitter.instruction("mov rax, rsi"); // __rt_str_persist reads the command pointer from rax under the shared x86_64 ABI + emitter.instruction("call __rt_str_persist"); // allocate an owned command copy for later status queries + emitter.instruction("test rax, rax"); // did command persistence allocate successfully? + emitter.instruction("jz __rt_proc_status_register_oom"); // leave proc_open usable when metadata allocation fails + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // retain persisted command pointer + emitter.instruction("mov QWORD PTR [rbp - 40], rdx"); // retain persisted command length + emitter.instruction("mov rax, 64"); // eight machine words form one status registry node + emitter.instruction("call __rt_heap_alloc"); // allocate raw metadata storage + emitter.instruction("test rax, rax"); // did the node allocation succeed? + emitter.instruction("jz __rt_proc_status_register_node_oom"); // release the copied command before falling back + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // retain the node pointer while linking it + abi::emit_symbol_address(emitter, "r10", "_proc_status_registry_head"); + emitter.instruction("mov r11, QWORD PTR [r10]"); // load the previous registry head + emitter.instruction("mov QWORD PTR [rax], r11"); // node.next = previous head + emitter.instruction("mov r11, QWORD PTR [rbp - 8]"); // reload the process HANDLE + emitter.instruction("mov QWORD PTR [rax + 8], r11"); // node.process = HANDLE + emitter.instruction("mov r11, QWORD PTR [rbp - 32]"); // reload the owned command pointer + emitter.instruction("mov QWORD PTR [rax + 16], r11"); // node.command_ptr = owned string + emitter.instruction("mov r11, QWORD PTR [rbp - 40]"); // reload the owned command length + emitter.instruction("mov QWORD PTR [rax + 24], r11"); // node.command_len = byte length + emitter.instruction("mov QWORD PTR [rax + 32], 0"); // clear reserved cached-status flags + emitter.instruction("mov QWORD PTR [rax + 40], 0"); // clear reserved exit-code state + emitter.instruction("mov QWORD PTR [rax + 48], 0"); // clear reserved termination-signal state + emitter.instruction("mov QWORD PTR [rax + 56], 0"); // clear reserved stop-signal state + emitter.instruction("mov QWORD PTR [r10], rax"); // publish the node only after full initialization + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // return the original live process HANDLE + emitter.instruction("add rsp, 48"); // release registry spill slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // resume proc_open result handling + emitter.label("__rt_proc_status_register_node_oom"); + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // recover the owned command allocation + emitter.instruction("call __rt_decref_any"); // release it because no node can own it + emitter.label("__rt_proc_status_register_oom"); + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // preserve successful proc_open despite missing metadata + emitter.instruction("add rsp, 48"); // release registry spill slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // fall back without status metadata + emitter.label("__rt_proc_status_register_done"); + emitter.instruction("mov rax, rdi"); // return the unchanged proc_open failure sentinel + emitter.instruction("ret"); // no registry state exists for a failed process +} + +/// Registers a NUL-terminated marshalled Windows command line. +fn emit_registry_register_cstr_windows(emitter: &mut Emitter) { + emitter.label_global("__rt_proc_status_register_cstr"); + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable C-string scan frame + emitter.instruction("sub rsp, 16"); // retain the process HANDLE and byte cursor + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the raw process HANDLE + emitter.instruction("xor edx, edx"); // begin at byte offset zero + emitter.label("__rt_proc_status_register_cstr_scan"); + emitter.instruction("cmp BYTE PTR [rsi + rdx], 0"); // reached the marshalled command terminator? + emitter.instruction("je __rt_proc_status_register_cstr_ready"); // the cursor is the exact command length + emitter.instruction("inc rdx"); // consume one non-NUL command byte + emitter.instruction("jmp __rt_proc_status_register_cstr_scan"); // continue the bounded NUL scan + emitter.label("__rt_proc_status_register_cstr_ready"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // restore the process HANDLE for the normal registry ABI + emitter.instruction("call __rt_proc_status_register"); // persist the counted command line + emitter.instruction("add rsp, 16"); // release the C-string scan frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the unchanged process HANDLE +} + +/// Removes and frees metadata after `proc_close` has consumed a process HANDLE. +fn emit_registry_unregister_windows(emitter: &mut Emitter) { + emitter.label_global("__rt_proc_status_unregister"); + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable unlink frame + emitter.instruction("sub rsp, 32"); // retain process, previous node, and match node + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the process HANDLE for return + abi::emit_symbol_address(emitter, "r10", "_proc_status_registry_head"); + emitter.instruction("mov r11, QWORD PTR [r10]"); // begin at the linked-list head + emitter.instruction("mov QWORD PTR [rbp - 16], 0"); // previous node = null + emitter.label("__rt_proc_status_unregister_find"); + emitter.instruction("test r11, r11"); // reached the end of the metadata list? + emitter.instruction("jz __rt_proc_status_unregister_done"); // missing metadata is harmless for proc_close + emitter.instruction("cmp QWORD PTR [r11 + 8], rdi"); // is this node associated with the process HANDLE? + emitter.instruction("je __rt_proc_status_unregister_found"); // unlink the matching node + emitter.instruction("mov QWORD PTR [rbp - 16], r11"); // advance the predecessor pointer + emitter.instruction("mov r11, QWORD PTR [r11]"); // continue through the linked list + emitter.instruction("jmp __rt_proc_status_unregister_find"); // inspect the next node + emitter.label("__rt_proc_status_unregister_found"); + emitter.instruction("mov QWORD PTR [rbp - 24], r11"); // retain the matched node across cleanup calls + emitter.instruction("mov rax, QWORD PTR [r11]"); // load its successor before unlinking + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the predecessor pointer + emitter.instruction("test r10, r10"); // did the match occur at the list head? + emitter.instruction("jnz __rt_proc_status_unregister_link_prev"); // non-head nodes relink through their predecessor + abi::emit_symbol_address(emitter, "r10", "_proc_status_registry_head"); + emitter.instruction("mov QWORD PTR [r10], rax"); // remove the head node + emitter.instruction("jmp __rt_proc_status_unregister_unlinked"); // release node-owned allocations + emitter.label("__rt_proc_status_unregister_link_prev"); + emitter.instruction("mov QWORD PTR [r10], rax"); // bypass the matched node + emitter.label("__rt_proc_status_unregister_unlinked"); + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the detached metadata node + emitter.instruction("mov rax, QWORD PTR [r11 + 16]"); // load its owned command string + emitter.instruction("call __rt_decref_any"); // release the command allocation + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // reload the detached raw node + emitter.instruction("call __rt_heap_free"); // return metadata storage to the runtime heap + emitter.label("__rt_proc_status_unregister_done"); + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // return the original process HANDLE to proc_close + emitter.instruction("add rsp, 32"); // release unlink spill slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // continue with process wait and CloseHandle +} + +/// Builds PHP's Windows `proc_get_status()` associative result without consuming the HANDLE. +fn emit_proc_get_status_windows(emitter: &mut Emitter) { + emitter.label_global("__rt_proc_get_status"); + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a status-query frame + emitter.instruction("sub rsp, 128"); // reserve metadata, Win32 output, and hash state with shadow space + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the live process HANDLE + abi::emit_symbol_address(emitter, "r10", "_proc_status_registry_head"); + emitter.instruction("mov r11, QWORD PTR [r10]"); // begin metadata lookup at the registry head + emitter.label("__rt_proc_get_status_find"); + emitter.instruction("test r11, r11"); // did lookup reach the end of the list? + emitter.instruction("jz __rt_proc_get_status_missing"); // unknown/closed resources cannot produce a status record + emitter.instruction("cmp QWORD PTR [r11 + 8], rdi"); // does this node own the requested HANDLE? + emitter.instruction("je __rt_proc_get_status_found"); // retain the matching metadata node + emitter.instruction("mov r11, QWORD PTR [r11]"); // follow the next node + emitter.instruction("jmp __rt_proc_get_status_find"); // continue registry lookup + emitter.label("__rt_proc_get_status_found"); + emitter.instruction("mov QWORD PTR [rbp - 16], r11"); // retain the metadata node across Win32 calls + emitter.instruction("mov rcx, QWORD PTR [rbp - 8]"); // GetProcessId receives the live process HANDLE + emitter.instruction("call GetProcessId"); // obtain PHP's numeric child pid field + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the PID for hash construction + emitter.instruction("mov rcx, QWORD PTR [rbp - 8]"); // GetExitCodeProcess receives the live process HANDLE + emitter.instruction("lea rdx, [rbp - 32]"); // provide the DWORD exit-code out parameter + emitter.instruction("call GetExitCodeProcess"); // query state without waiting or closing + emitter.instruction("test eax, eax"); // did the Win32 status query succeed? + emitter.instruction("jz __rt_proc_get_status_query_failed"); // use a safe non-running fallback on API failure + emitter.instruction("mov eax, DWORD PTR [rbp - 32]"); // load the child exit status DWORD + emitter.instruction("cmp eax, 259"); // STILL_ACTIVE means the child remains running + emitter.instruction("sete al"); // record the PHP running boolean + emitter.instruction("movzx eax, al"); // widen the boolean to one machine word + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the running flag + emitter.instruction("cmp rax, 0"); // did status report a live process? + emitter.instruction("jne __rt_proc_get_status_exit_ready"); // live processes report exitcode -1 + emitter.instruction("movsxd rax, DWORD PTR [rbp - 32]"); // preserve the exit code as a SIGNED 32-bit value, matching + // php storing the DWORD into `proc->exit_code` (an int) + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save completed exit code for the status hash + emitter.instruction("jmp __rt_proc_get_status_hash"); // build the PHP status record + emitter.label("__rt_proc_get_status_exit_ready"); + emitter.instruction("mov QWORD PTR [rbp - 48], -1"); // running processes expose PHP exitcode -1 + emitter.instruction("jmp __rt_proc_get_status_hash"); // build the PHP status record + emitter.label("__rt_proc_get_status_query_failed"); + emitter.instruction("mov QWORD PTR [rbp - 40], 0"); // failed status query cannot report a live process + emitter.instruction("mov QWORD PTR [rbp - 48], -1"); // preserve PHP's unknown-exit sentinel + emitter.label("__rt_proc_get_status_hash"); + emitter.instruction("mov rdi, 16"); // allocate enough initial buckets for all nine fields + emitter.instruction("mov rsi, 7"); // status values are heterogeneous boxed Mixed values + emitter.instruction("call __rt_hash_new"); // create the associative status hash + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // retain the possibly-moving hash pointer + emitter.instruction("mov r11, QWORD PTR [rbp - 16]"); // reload metadata for the persisted command + emitter.instruction("mov rcx, QWORD PTR [r11 + 16]"); // value low word = registry-owned command pointer + emitter.instruction("mov r8, QWORD PTR [r11 + 24]"); // value high word = command byte length + emitter.instruction("mov r9, 1"); // runtime tag 1 = string + emit_hash_put_windows(emitter, "_proc_status_k_command", 7); + emitter.instruction("mov rcx, QWORD PTR [rbp - 24]"); // value low word = numeric child PID + emitter.instruction("xor r8d, r8d"); // integer values have no high payload word + emitter.instruction("xor r9d, r9d"); // runtime tag 0 = integer + emit_hash_put_windows(emitter, "_proc_status_k_pid", 3); + emit_hash_bool_windows(emitter, "_proc_status_k_cached", 6, 0); + emitter.instruction("mov rcx, QWORD PTR [rbp - 40]"); // value low word = queried running boolean + emitter.instruction("xor r8d, r8d"); // boolean values have no high payload word + emitter.instruction("mov r9, 3"); // runtime tag 3 = boolean + emit_hash_put_windows(emitter, "_proc_status_k_running", 7); + emit_hash_bool_windows(emitter, "_proc_status_k_signaled", 8, 0); + emit_hash_bool_windows(emitter, "_proc_status_k_stopped", 7, 0); + emitter.instruction("mov rcx, QWORD PTR [rbp - 48]"); // value low word = completed code or PHP -1 sentinel + emitter.instruction("xor r8d, r8d"); // integer values have no high payload word + emitter.instruction("xor r9d, r9d"); // runtime tag 0 = integer + emit_hash_put_windows(emitter, "_proc_status_k_exitcode", 8); + emit_hash_int_windows(emitter, "_proc_status_k_termsig", 7, 0); + emit_hash_int_windows(emitter, "_proc_status_k_stopsig", 7, 0); + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // return the completed associative status hash + emitter.instruction("add rsp, 128"); // release status frame and MSx64 shadow space + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // leave the HANDLE live for later proc_close + emitter.label("__rt_proc_get_status_missing"); + emitter.instruction("xor eax, eax"); // no retained process metadata maps to PHP false + emitter.instruction("add rsp, 128"); // release status frame and MSx64 shadow space + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return null hash pointer to the lowerer +} + +/// Boxes the staged concrete value and inserts it under one static status key. +fn emit_hash_put_windows(emitter: &mut Emitter, key: &str, len: i64) { + emitter.instruction("mov rax, r9"); // move the concrete value tag into the mixed-box ABI + emitter.instruction("mov rdi, rcx"); // move the concrete low payload into the mixed-box ABI + emitter.instruction("mov rsi, r8"); // move the concrete high payload into the mixed-box ABI + emitter.instruction("call __rt_mixed_from_value"); // give this heterogeneous hash entry its own boxed owner + emitter.instruction("mov rcx, rax"); // hash value low word = boxed Mixed cell + emitter.instruction("xor r8d, r8d"); // boxed Mixed values have no separate high word + emitter.instruction("mov r9, 7"); // entry tag 7 identifies a boxed Mixed cell + abi::emit_symbol_address(emitter, "rsi", key); + emitter.instruction(&format!("mov rdx, {len}")); // pass the static key byte length to hash_set + emitter.instruction("mov rdi, QWORD PTR [rbp - 56]"); // reload the current possibly-grown status hash + emitter.instruction("call __rt_hash_set"); // insert one PHP status field + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // retain the potentially reallocated hash pointer +} + +/// Inserts a literal boolean status field. +fn emit_hash_bool_windows(emitter: &mut Emitter, key: &str, len: i64, value: i64) { + emitter.instruction(&format!("mov rcx, {value}")); // stage the boolean payload word + emitter.instruction("xor r8d, r8d"); // booleans have no high payload word + emitter.instruction("mov r9, 3"); // runtime tag 3 = boolean + emit_hash_put_windows(emitter, key, len); +} + +/// Inserts a literal integer status field. +fn emit_hash_int_windows(emitter: &mut Emitter, key: &str, len: i64, value: i64) { + emitter.instruction(&format!("mov rcx, {value}")); // stage the integer payload word + emitter.instruction("xor r8d, r8d"); // integers have no high payload word + emitter.instruction("xor r9d, r9d"); // runtime tag 0 = integer + emit_hash_put_windows(emitter, key, len); +} + +#[cfg(test)] +mod tests { + //! Purpose: + //! Structural tests for process status and termination runtime emission. + //! + //! Called from: + //! - `cargo test` through Rust's test harness. + //! + //! Key details: + //! - Windows checks require HANDLE-preserving API calls and every PHP status key. + + use super::*; + use crate::codegen_support::platform::Target; + + /// Verifies Windows status preserves its live HANDLE and builds all PHP fields. + #[test] + fn windows_proc_status_uses_live_handle_and_all_php_keys() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_proc_status(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("__rt_proc_get_status")); + assert!(asm.contains("call GetProcessId")); + assert!(asm.contains("call GetExitCodeProcess")); + assert!(asm.contains("call __rt_mixed_from_value")); + assert!(!asm.contains("call CloseHandle")); + for key in [ + "_proc_status_k_command", + "_proc_status_k_pid", + "_proc_status_k_cached", + "_proc_status_k_running", + "_proc_status_k_signaled", + "_proc_status_k_stopped", + "_proc_status_k_exitcode", + "_proc_status_k_termsig", + "_proc_status_k_stopsig", + ] { + assert!(asm.contains(key), "missing status key {key}"); + } + } + + /// Verifies the Windows process-status registry passes command bytes to the + /// shared string persister through its x86_64 result-register ABI. + #[test] + fn windows_proc_status_persists_command_with_string_runtime_abi() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_proc_status(&mut emitter); + let asm = emitter.output(); + let register = asm + .split("__rt_proc_status_register:") + .nth(1) + .and_then(|section| section.split("__rt_proc_status_register_cstr:").next()) + .expect("Windows status-register emitter must be present"); + assert!(register.contains("mov rax, rsi")); + assert!(register.contains("call __rt_str_persist")); + assert!(!register.contains("mov rdi, rsi")); + } + + /// Verifies each supported process ABI emits an explicit terminate primitive. + #[test] + fn proc_terminate_has_target_specific_process_primitives() { + for target in [ + Target::new(Platform::MacOS, Arch::AArch64), + Target::new(Platform::Linux, Arch::AArch64), + Target::new(Platform::Linux, Arch::X86_64), + Target::new(Platform::Windows, Arch::X86_64), + ] { + let mut emitter = Emitter::new(target); + emit_proc_status(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("__rt_proc_terminate")); + } + } +} diff --git a/src/codegen_support/runtime/io/readdir.rs b/src/codegen_support/runtime/io/readdir.rs index 539c5728f8..b187a9a2a5 100644 --- a/src/codegen_support/runtime/io/readdir.rs +++ b/src/codegen_support/runtime/io/readdir.rs @@ -10,6 +10,10 @@ //! `readdir`; the `d_name` field is copied to the heap so the name survives //! the next `readdir`/`closedir` call. //! - A null pointer result marks end-of-directory and is boxed as PHP `false`. +//! - The x86_64 `readdir` call site routes through `Emitter::emit_call_c`. On +//! Windows, `__rt_sys_readdir` consumes the pending `FindFirstFileExW` result +//! and advances with `FindNextFileW`, exposing the name through a compatible +//! scratch `dirent`. Other targets continue to use libc. use crate::codegen_support::{abi, emit::Emitter, platform::Arch}; @@ -133,7 +137,7 @@ fn emit_readdir_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("test r10, r10"); // was a DIR* recorded for this descriptor? emitter.instruction("jz __rt_readdir_end_x86"); // no handle recorded: report end of directory emitter.instruction("mov rdi, r10"); // DIR* argument for readdir - emitter.bl_c("readdir"); + emitter.emit_call_c("readdir"); // Windows: advance the FindFirstFileExW/FindNextFileW enumeration emitter.instruction("test rax, rax"); // a NULL dirent means no more entries emitter.instruction("jz __rt_readdir_end_x86"); // report end of directory once entries run out diff --git a/src/codegen_support/runtime/io/resolve_host.rs b/src/codegen_support/runtime/io/resolve_host.rs index 67924e710d..4ef709024b 100644 --- a/src/codegen_support/runtime/io/resolve_host.rs +++ b/src/codegen_support/runtime/io/resolve_host.rs @@ -75,7 +75,7 @@ fn emit_resolve_host_linux_x86_64(emitter: &mut Emitter) { // -- resolve the name through libc gethostbyname -- emitter.instruction("mov rdi, rax"); // host name into the gethostbyname argument register - emitter.instruction("call gethostbyname"); // rax = struct hostent* (null when unresolved) + emitter.emit_call_c("gethostbyname"); // rax = struct hostent* (null when unresolved) emitter.instruction("test rax, rax"); // did resolution fail? emitter.instruction("jz __rt_resolve_host_fail_x86"); // a null hostent means resolution failed emitter.instruction("mov rax, QWORD PTR [rax + 24]"); // hostent.h_addr_list diff --git a/src/codegen_support/runtime/io/resolve_host_v6.rs b/src/codegen_support/runtime/io/resolve_host_v6.rs index 56eb31096c..dfdff4de03 100644 --- a/src/codegen_support/runtime/io/resolve_host_v6.rs +++ b/src/codegen_support/runtime/io/resolve_host_v6.rs @@ -149,7 +149,7 @@ fn emit_resolve_host_v6_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("xor esi, esi"); // arg 2: service = NULL emitter.instruction("lea rdx, [rbp - 48]"); // arg 3: &hints emitter.instruction("lea rcx, [rbp - 56]"); // arg 4: &res - emitter.instruction("call getaddrinfo"); // call external helper + emitter.emit_call_c("getaddrinfo"); // call external helper emitter.instruction("test rax, rax"); // check whether the runtime value is zero emitter.instruction("jnz __rt_rhv6_fail_x86"); // non-zero return = error @@ -168,7 +168,7 @@ fn emit_resolve_host_v6_linux_x86_64(emitter: &mut Emitter) { // -- freeaddrinfo(res) -- emitter.instruction("mov rdi, QWORD PTR [rbp - 56]"); // prepare SysV call argument - emitter.instruction("call freeaddrinfo"); // call external helper + emitter.emit_call_c("freeaddrinfo"); // call external helper emitter.instruction("mov eax, 1"); // success emitter.instruction("add rsp, 80"); // release runtime stack frame @@ -177,7 +177,7 @@ fn emit_resolve_host_v6_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_rhv6_free_fail_x86"); emitter.instruction("mov rdi, QWORD PTR [rbp - 56]"); // prepare SysV call argument - emitter.instruction("call freeaddrinfo"); // call external helper + emitter.emit_call_c("freeaddrinfo"); // call external helper // fall through emitter.label("__rt_rhv6_fail_x86"); emitter.instruction("xor eax, eax"); // failure diff --git a/src/codegen_support/runtime/io/rewinddir.rs b/src/codegen_support/runtime/io/rewinddir.rs index cbdf5b7de6..af27a47f21 100644 --- a/src/codegen_support/runtime/io/rewinddir.rs +++ b/src/codegen_support/runtime/io/rewinddir.rs @@ -9,6 +9,9 @@ //! - The `DIR*` recorded by `__rt_opendir` in `_dir_handles` is handed to libc //! `rewinddir`; the handle stays registered so later `readdir()` calls reuse it. //! - A descriptor with no recorded `DIR*` is a no-op. +//! - The x86_64 `rewinddir` call site routes through `Emitter::emit_call_c`. On +//! Windows, `__rt_sys_rewinddir` closes and reopens the `FindFirstFileExW` +//! search against its retained pattern. Other targets continue to use libc. use crate::codegen_support::{abi, emit::Emitter, platform::Arch}; @@ -73,7 +76,7 @@ fn emit_rewinddir_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("test r10, r10"); // was a DIR* recorded for this descriptor? emitter.instruction("jz __rt_rewinddir_done_x86"); // nothing recorded: nothing to rewind emitter.instruction("mov rdi, r10"); // DIR* argument for rewinddir - emitter.bl_c("rewinddir"); + emitter.emit_call_c("rewinddir"); // Windows: close and reopen the retained FindFirstFileExW search emitter.label("__rt_rewinddir_done_x86"); emitter.instruction("pop rbp"); // restore the caller frame pointer diff --git a/src/codegen_support/runtime/io/scandir.rs b/src/codegen_support/runtime/io/scandir.rs index 2d6f974a9c..05c7d9259e 100644 --- a/src/codegen_support/runtime/io/scandir.rs +++ b/src/codegen_support/runtime/io/scandir.rs @@ -7,6 +7,10 @@ //! //! Key details: //! - I/O helpers bridge PHP strings, resources, descriptors, and libc calls while returning runtime arrays or pointer/length strings. +//! - The x86_64 `opendir`/`readdir`/`closedir` call sites route through +//! `Emitter::emit_call_c`. On Windows, their shims enumerate with +//! `FindFirstFileExW`/`FindNextFileW` and release the search with `FindClose`; +//! other targets continue to use libc. use crate::codegen_support::{emit::Emitter, platform::Arch}; @@ -112,14 +116,14 @@ fn emit_scandir_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("call __rt_array_new"); // allocate the destination string array that will collect the directory entry names emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // preserve the destination string array pointer across the directory iteration loop emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the C directory path pointer before opening the directory stream - emitter.instruction("call opendir"); // open the directory stream through libc opendir() + emitter.emit_call_c("opendir"); // open the directory stream (Windows: FindFirstFileExW-backed shim) emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // preserve the DIR* handle across the readdir() loop and the final closedir() call emitter.instruction("test rax, rax"); // detect opendir() failure before entering the directory iteration loop emitter.instruction("jz __rt_scandir_ret"); // return the empty result array when the directory stream cannot be opened emitter.label("__rt_scandir_loop"); emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload the DIR* handle before asking libc for the next directory entry - emitter.instruction("call readdir"); // fetch the next directory entry through libc readdir() + emitter.emit_call_c("readdir"); // fetch the next entry (Windows: FindNextFileW-backed shim) emitter.instruction("test rax, rax"); // detect the end-of-directory marker before measuring a filename or appending it emitter.instruction("jz __rt_scandir_close"); // stop iterating once libc readdir() reports that no more directory entries remain emitter.instruction(&format!("lea rsi, [rax + {}]", name_off)); // compute the pointer to dirent.d_name for the current Linux directory entry layout @@ -139,7 +143,7 @@ fn emit_scandir_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_scandir_close"); emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload the DIR* handle before closing the directory stream - emitter.instruction("call closedir"); // close the directory stream through libc closedir() + emitter.emit_call_c("closedir"); // close the directory stream (Windows: FindClose-backed shim) emitter.label("__rt_scandir_ret"); emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // return the destination string array pointer in the canonical x86_64 integer result register diff --git a/src/codegen_support/runtime/io/stash_connect_host.rs b/src/codegen_support/runtime/io/stash_connect_host.rs index 6f083ddb3b..49e1943206 100644 --- a/src/codegen_support/runtime/io/stash_connect_host.rs +++ b/src/codegen_support/runtime/io/stash_connect_host.rs @@ -17,8 +17,8 @@ //! a bracketless form via the peer-name path; an IP SNI is simply ignored by a //! server, which is the safe PHP-compatible outcome. //! - The host bytes are persisted with `__rt_str_persist` (heap-owned, never -//! freed — one slot per fd, overwritten on the next connect to the same fd) and -//! stored as `(ptr, len)` at `_stream_connect_host[fd * 16]`. +//! freed — one slot per live descriptor, overwritten on reconnect) and stored +//! in a bounded associative full-width-descriptor map. //! - The fd passes through unchanged (in on arg0, out in the result register) so //! the builtin can wrap the call transparently. @@ -28,9 +28,8 @@ use crate::codegen_support::{abi, emit::Emitter, platform::Arch}; /// /// Inputs (AArch64): x0 = fd, x1 = address pointer, x2 = address length. /// (x86_64): rdi = fd, rsi = address pointer, rdx = address length. Output: -/// x0 / rax = fd (unchanged). When `fd < 0` or `fd >= 256` the helper is a no-op -/// passthrough so failed connects and out-of-range descriptors never touch the -/// table. +/// x0 / rax = fd (unchanged). Negative descriptors are passthrough failures; +/// full-width Winsock handles are accepted and matched as keys rather than indexes. pub fn emit_stash_connect_host(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_stash_connect_host_linux_x86_64(emitter); @@ -48,11 +47,9 @@ pub fn emit_stash_connect_host(emitter: &mut Emitter) { emitter.instruction("add x29, sp, #32"); // establish the helper frame pointer emitter.instruction("str x0, [sp, #0]"); // save the fd for the table index and return value - // -- range-check the fd: only descriptors 0..256 get a table slot -- + // -- reject only failed connects; valid descriptors are full-width keys -- emitter.instruction("cmp x0, #0"); // is the fd negative (failed connect)? emitter.instruction("b.lt __rt_sch_done"); // negative fd → passthrough, no stash - emitter.instruction("cmp x0, #256"); // is the fd within the table bound? - emitter.instruction("b.ge __rt_sch_done"); // out-of-range fd → passthrough, no stash // -- strip a leading "scheme://" by scanning for "://" -- emitter.instruction("mov x9, #0"); // host start offset within the address @@ -102,19 +99,50 @@ pub fn emit_stash_connect_host(emitter: &mut Emitter) { emitter.label("__rt_sch_persist"); emitter.instruction("mov x2, x11"); // x2 = host length (trimmed of any :port) - // -- persist the host bytes (heap-owned) and store (ptr, len) in the table -- + // -- persist the host bytes and associate them with the full-width fd -- abi::emit_call_label(emitter, "__rt_str_persist"); // x1 = persisted host ptr, x2 = len (unchanged) + emitter.instruction("str x1, [sp, #8]"); // preserve the persisted host pointer during the slot scan + emitter.instruction("str x2, [sp, #16]"); // preserve the persisted host length during the slot scan + abi::emit_symbol_address(emitter, "x13", "_stream_connect_host_fds"); abi::emit_symbol_address(emitter, "x10", "_stream_connect_host"); - emitter.instruction("ldr x9, [sp, #0]"); // reload the fd for the table index - emitter.instruction("add x10, x10, x9, lsl #4"); // slot = table + fd * 16 (ptr, len pair) - emitter.instruction("str x1, [x10, #0]"); // _stream_connect_host[fd].ptr = persisted host - emitter.instruction("str x2, [x10, #8]"); // _stream_connect_host[fd].len = host length + emitter.instruction("ldr x12, [sp, #0]"); // reload the full-width descriptor key + emitter.instruction("mov x9, #0"); // begin with an existing-key scan + emitter.label("__rt_sch_slot_match"); + emitter.instruction("cmp x9, #256"); // has every existing host slot been inspected? + emitter.instruction("b.ge __rt_sch_slot_empty_start"); // no match requires a free-slot scan + emitter.instruction("add x11, x10, x9, lsl #4"); // address this candidate host pointer/length pair + emitter.instruction("ldr x14, [x11, #8]"); // load the candidate host length marker + emitter.instruction("cbz x14, __rt_sch_slot_match_next"); // unused slots cannot match a descriptor + emitter.instruction("ldr x14, [x13, x9, lsl #3]"); // load this slot's full-width descriptor key + emitter.instruction("cmp x14, x12"); // is this the descriptor being updated? + emitter.instruction("b.eq __rt_sch_slot_store"); // replace the existing host value in place + emitter.label("__rt_sch_slot_match_next"); + emitter.instruction("add x9, x9, #1"); // advance through occupied slots + emitter.instruction("b __rt_sch_slot_match"); // continue the existing-key scan + emitter.label("__rt_sch_slot_empty_start"); + emitter.instruction("mov x9, #0"); // restart at slot zero to find free capacity + emitter.label("__rt_sch_slot_empty"); + emitter.instruction("cmp x9, #256"); // has every bounded slot proved occupied? + emitter.instruction("b.ge __rt_sch_done"); // full host metadata table leaves the descriptor unstashed + emitter.instruction("add x11, x10, x9, lsl #4"); // address this candidate host pointer/length pair + emitter.instruction("ldr x14, [x11, #8]"); // inspect the candidate host length marker + emitter.instruction("cbz x14, __rt_sch_slot_store"); // zero length identifies an available slot + emitter.instruction("add x9, x9, #1"); // advance to the next candidate free slot + emitter.instruction("b __rt_sch_slot_empty"); // continue the bounded free-slot scan + emitter.label("__rt_sch_slot_store"); + emitter.instruction("str x12, [x13, x9, lsl #3]"); // store the full-width descriptor key + emitter.instruction("ldr x1, [sp, #8]"); // reload the persisted host pointer + emitter.instruction("ldr x2, [sp, #16]"); // reload the persisted host length + emitter.instruction("str x1, [x11, #0]"); // store the persisted host pointer + emitter.instruction("str x2, [x11, #8]"); // publish the non-zero host length last emitter.label("__rt_sch_done"); emitter.instruction("ldr x0, [sp, #0]"); // return the fd unchanged 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 stream_socket_client emitter + + emit_get_stashed_connect_host_aarch64(emitter); } /// x86_64 implementation of `__rt_stash_connect_host`. @@ -130,11 +158,9 @@ fn emit_stash_connect_host_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("sub rsp, 16"); // spill slot for the fd across str_persist emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the fd for the table index and return value - // -- range-check the fd: only descriptors 0..256 get a table slot -- + // -- reject only failed connects; valid descriptors are full-width keys -- emitter.instruction("cmp rdi, 0"); // is the fd negative (failed connect)? emitter.instruction("jl __rt_sch_done_x86"); // negative fd → passthrough, no stash - emitter.instruction("cmp rdi, 256"); // is the fd within the table bound? - emitter.instruction("jge __rt_sch_done_x86"); // out-of-range fd → passthrough, no stash // -- strip a leading "scheme://" by scanning for "://" (rsi=ptr, rdx=len) -- emitter.instruction("xor r9, r9"); // host start offset within the address @@ -182,16 +208,122 @@ fn emit_stash_connect_host_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rax, rsi"); // str_persist source pointer = host pointer emitter.instruction("mov rdx, r11"); // str_persist length = trimmed host length abi::emit_call_label(emitter, "__rt_str_persist"); // rax = persisted host ptr, rdx = len (unchanged) - abi::emit_symbol_address(emitter, "r10", "_stream_connect_host"); // base of the per-fd connect-host table - emitter.instruction("mov r9, QWORD PTR [rbp - 8]"); // reload the fd for the table index - emitter.instruction("shl r9, 4"); // fd * 16 (ptr, len pair stride) - emitter.instruction("add r10, r9"); // slot address = table + fd * 16 - emitter.instruction("mov QWORD PTR [r10 + 0], rax"); // _stream_connect_host[fd].ptr = persisted host - emitter.instruction("mov QWORD PTR [r10 + 8], rdx"); // _stream_connect_host[fd].len = host length + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // preserve the persisted host pointer during the slot scan + abi::emit_symbol_address(emitter, "r8", "_stream_connect_host_fds"); + abi::emit_symbol_address(emitter, "r9", "_stream_connect_host"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the full-width descriptor key + emitter.instruction("xor r10d, r10d"); // begin with an existing-key scan + emitter.label("__rt_sch_slot_match_x86"); + emitter.instruction("cmp r10, 256"); // has every existing host slot been inspected? + emitter.instruction("jge __rt_sch_slot_empty_start_x86"); // no match requires a free-slot scan + emitter.instruction("mov r11, r10"); // copy the slot index for the 16-byte host-pair stride + emitter.instruction("shl r11, 4"); // convert the slot index to a host-pair byte offset + emitter.instruction("cmp QWORD PTR [r9 + r11 + 8], 0"); // is this host slot unused? + emitter.instruction("je __rt_sch_slot_match_next_x86"); // unused slots cannot match a descriptor + emitter.instruction("cmp QWORD PTR [r8 + r10 * 8], rdi"); // is this the descriptor being updated? + emitter.instruction("je __rt_sch_slot_store_x86"); // replace the existing host value in place + emitter.label("__rt_sch_slot_match_next_x86"); + emitter.instruction("add r10, 1"); // advance through occupied slots + emitter.instruction("jmp __rt_sch_slot_match_x86"); // continue the existing-key scan + emitter.label("__rt_sch_slot_empty_start_x86"); + emitter.instruction("xor r10d, r10d"); // restart at slot zero to find free capacity + emitter.label("__rt_sch_slot_empty_x86"); + emitter.instruction("cmp r10, 256"); // has every bounded slot proved occupied? + emitter.instruction("jge __rt_sch_done_x86"); // full host metadata table leaves the descriptor unstashed + emitter.instruction("mov r11, r10"); // copy the slot index for the 16-byte host-pair stride + emitter.instruction("shl r11, 4"); // convert the slot index to a host-pair byte offset + emitter.instruction("cmp QWORD PTR [r9 + r11 + 8], 0"); // inspect the candidate host length marker + emitter.instruction("je __rt_sch_slot_store_x86"); // zero length identifies an available slot + emitter.instruction("add r10, 1"); // advance to the next candidate free slot + emitter.instruction("jmp __rt_sch_slot_empty_x86"); // continue the bounded free-slot scan + emitter.label("__rt_sch_slot_store_x86"); + emitter.instruction("mov QWORD PTR [r8 + r10 * 8], rdi"); // store the full-width descriptor key + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the persisted host pointer + emitter.instruction("mov QWORD PTR [r9 + r11 + 0], rax"); // store the persisted host pointer + emitter.instruction("mov QWORD PTR [r9 + r11 + 8], rdx"); // publish the non-zero host length last emitter.label("__rt_sch_done_x86"); emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // return the fd unchanged emitter.instruction("add rsp, 16"); // release the helper frame emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return to the stream_socket_client emitter + + emit_get_stashed_connect_host_x86_64(emitter); +} + +/// Emits the AArch64 full-width descriptor lookup for persisted SNI host metadata. +fn emit_get_stashed_connect_host_aarch64(emitter: &mut Emitter) { + emitter.label_global("__rt_get_stashed_connect_host"); + abi::emit_symbol_address(emitter, "x9", "_stream_connect_host_fds"); + abi::emit_symbol_address(emitter, "x10", "_stream_connect_host"); + emitter.instruction("mov x11, #0"); // begin at the first bounded host slot + emitter.label("__rt_gsch_loop"); + emitter.instruction("cmp x11, #256"); // have all persisted host slots been searched? + emitter.instruction("b.ge __rt_gsch_miss"); // no matching descriptor means no default SNI host + emitter.instruction("add x12, x10, x11, lsl #4"); // address this candidate host pointer/length pair + emitter.instruction("ldr x2, [x12, #8]"); // load the candidate host length marker + emitter.instruction("cbz x2, __rt_gsch_next"); // unused slots cannot match a descriptor + emitter.instruction("ldr x13, [x9, x11, lsl #3]"); // load this slot's full-width descriptor key + emitter.instruction("cmp x13, x0"); // does the descriptor match the requested socket? + emitter.instruction("b.eq __rt_gsch_found"); // return this persisted host pair + emitter.label("__rt_gsch_next"); + emitter.instruction("add x11, x11, #1"); // advance to the next bounded host slot + emitter.instruction("b __rt_gsch_loop"); // continue the associative lookup + emitter.label("__rt_gsch_found"); + emitter.instruction("ldr x1, [x12, #0]"); // return the persisted host pointer + emitter.instruction("mov x0, #1"); // report a successful metadata lookup + emitter.instruction("ret"); // return the host pointer/length pair + emitter.label("__rt_gsch_miss"); + emitter.instruction("mov x0, #0"); // report that no persisted host is available + emitter.instruction("mov x1, #0"); // clear the absent host pointer + emitter.instruction("mov x2, #0"); // clear the absent host length + emitter.instruction("ret"); // return the lookup miss +} + +/// Emits the x86_64 full-width descriptor lookup for persisted SNI host metadata. +fn emit_get_stashed_connect_host_x86_64(emitter: &mut Emitter) { + emitter.label_global("__rt_get_stashed_connect_host"); + abi::emit_symbol_address(emitter, "r8", "_stream_connect_host_fds"); + abi::emit_symbol_address(emitter, "r9", "_stream_connect_host"); + emitter.instruction("xor r10d, r10d"); // begin at the first bounded host slot + emitter.label("__rt_gsch_loop_x86"); + emitter.instruction("cmp r10, 256"); // have all persisted host slots been searched? + emitter.instruction("jge __rt_gsch_miss_x86"); // no matching descriptor means no default SNI host + emitter.instruction("mov r11, r10"); // copy the slot index for the 16-byte host-pair stride + emitter.instruction("shl r11, 4"); // convert the slot index to a host-pair byte offset + emitter.instruction("mov rdx, QWORD PTR [r9 + r11 + 8]"); // load the candidate host length marker + emitter.instruction("test rdx, rdx"); // is this host slot live? + emitter.instruction("jz __rt_gsch_next_x86"); // unused slots cannot match a descriptor + emitter.instruction("cmp QWORD PTR [r8 + r10 * 8], rdi"); // does the descriptor match the requested socket? + emitter.instruction("je __rt_gsch_found_x86"); // return this persisted host pair + emitter.label("__rt_gsch_next_x86"); + emitter.instruction("add r10, 1"); // advance to the next bounded host slot + emitter.instruction("jmp __rt_gsch_loop_x86"); // continue the associative lookup + emitter.label("__rt_gsch_found_x86"); + emitter.instruction("mov rax, QWORD PTR [r9 + r11 + 0]"); // return the persisted host pointer + emitter.instruction("ret"); // return the host pointer/length pair + emitter.label("__rt_gsch_miss_x86"); + emitter.instruction("xor eax, eax"); // null pointer reports absent persisted host metadata + emitter.instruction("xor edx, edx"); // clear the absent host length + emitter.instruction("ret"); // return the lookup miss +} + +#[cfg(test)] +mod tests { + use crate::codegen_support::platform::{Platform, Target}; + + use super::*; + + /// Verifies Windows SNI metadata accepts full-width socket keys and scans + /// bounded slots instead of rejecting or directly indexing handles above 255. + #[test] + fn windows_connect_host_map_accepts_full_width_socket_keys() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_stash_connect_host(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("cmp QWORD PTR [r8 + r10 * 8], rdi")); + assert!(asm.contains("cmp r10, 256")); + assert!(!asm.contains("cmp rdi, 256")); + assert!(!asm.contains("shl rdi, 4")); + } } diff --git a/src/codegen_support/runtime/io/stat.rs b/src/codegen_support/runtime/io/stat.rs index 0f58af90c4..edbf262a19 100644 --- a/src/codegen_support/runtime/io/stat.rs +++ b/src/codegen_support/runtime/io/stat.rs @@ -214,14 +214,27 @@ pub fn emit_stat(emitter: &mut Emitter) { emitter.instruction("add x1, sp, #0"); // pointer to stat buffer on stack emitter.syscall(338); - // -- extract st_size from stat struct -- + // -- extract st_size, or report false when stat() failed -- + // php returns false for a path it cannot stat. Reading the buffer regardless + // handed back whatever the stack held; that it usually looked like 0 was luck, + // not correctness. + emitter.instruction("cmp x0, #0"); // did stat() succeed? + emitter.instruction("b.ne __rt_filesize_false"); // failure path: report PHP false emitter.instruction(&format!("ldr x0, [sp, #{}]", size_off)); // load st_size from stat struct + emitter.instruction("mov x1, #1"); // success flag for codegen-side int|false boxing // -- restore frame and return -- emitter.instruction(&format!("ldp x29, x30, [sp, #{}]", save_offset)); // restore frame pointer and return address emitter.instruction(&format!("add sp, sp, #{}", frame_size)); // deallocate stack frame emitter.instruction("ret"); // return to caller + emitter.label("__rt_filesize_false"); + emitter.instruction("mov x0, #0"); // stat failed: integer payload defaults to 0 + emitter.instruction("mov x1, #0"); // failure flag tells codegen to box PHP false + emitter.instruction(&format!("ldp x29, x30, [sp, #{}]", save_offset)); // restore frame pointer and return address (failure path) + emitter.instruction(&format!("add sp, sp, #{}", frame_size)); // deallocate stack frame (failure path) + emitter.instruction("ret"); // return the false flag + // ================================================================ // __rt_filemtime: get file modification time // Input: x1/x2=path @@ -241,13 +254,26 @@ pub fn emit_stat(emitter: &mut Emitter) { emitter.instruction("add x1, sp, #0"); // pointer to stat buffer on stack emitter.syscall(338); - // -- extract st_mtimespec.tv_sec from stat struct -- + // -- extract st_mtimespec.tv_sec, or report false when stat() failed -- + // php returns false for a path it cannot stat. Reading the buffer regardless + // handed back whatever the stack happened to hold, so the answer changed with + // the path and leaked stack bytes. + emitter.instruction("cmp x0, #0"); // did stat() succeed? + emitter.instruction("b.ne __rt_filemtime_false"); // failure path: report PHP false emitter.instruction(&format!("ldr x0, [sp, #{}]", mtime_off)); // load mtime tv_sec from stat struct + emitter.instruction("mov x1, #1"); // success flag for codegen-side int|false boxing // -- restore frame and return -- emitter.instruction(&format!("ldp x29, x30, [sp, #{}]", save_offset)); // restore frame pointer and return address emitter.instruction(&format!("add sp, sp, #{}", frame_size)); // deallocate stack frame emitter.instruction("ret"); // return to caller + + emitter.label("__rt_filemtime_false"); + emitter.instruction("mov x0, #0"); // stat failed: integer payload defaults to 0 + emitter.instruction("mov x1, #0"); // failure flag tells codegen to box PHP false + emitter.instruction(&format!("ldp x29, x30, [sp, #{}]", save_offset)); // restore frame pointer and return address (failure path) + emitter.instruction(&format!("add sp, sp, #{}", frame_size)); // deallocate stack frame (failure path) + emitter.instruction("ret"); // return the false flag } /// Emits x86_64 Linux stat helpers: __rt_file_exists, __rt_is_file, __rt_is_dir, @@ -255,9 +281,16 @@ pub fn emit_stat(emitter: &mut Emitter) { /// Uses the Linux `stat()` syscall via libc and the System V AMD64 ABI. /// Input: rdi=path pointer, rsi=path length (elephc string convention). Output: rax=result. fn emit_stat_linux_x86_64(emitter: &mut Emitter) { - let mode_off = 24usize; - let size_off = emitter.platform.stat_size_offset(); - let mtime_off = emitter.platform.stat_mtime_offset(); + // Windows routes the C symbol through `__rt_sys_stat`, which deliberately + // writes the portable runtime layout. Native Linux keeps glibc's x86_64 + // layout, whose mode field starts eight bytes later. + let mode_off = if emitter.platform == crate::codegen_support::platform::Platform::Windows { + emitter.platform.stat_mode_offset() + } else { + 24usize + }; + let size_off = 48usize; + let mtime_off = 88usize; let frame_size = 144usize; emitter.blank(); @@ -326,9 +359,11 @@ fn emit_stat_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("cmp eax, 0"); // test whether libc stat() succeeded before reading st_size emitter.instruction("jne __rt_filesize_fail"); // return zero when the stat call fails emitter.instruction(&format!("mov rax, QWORD PTR [rsp + {}]", size_off)); // load st_size from the Linux stat buffer + emitter.instruction("mov rdx, 1"); // success flag for codegen-side int|false boxing emitter.instruction("jmp __rt_filesize_ret"); // skip the failure path after reading the file size emitter.label("__rt_filesize_fail"); - emitter.instruction("mov rax, 0"); // return zero when the path could not be stated + emitter.instruction("mov rax, 0"); // stat failed: integer payload defaults to 0 + emitter.instruction("mov rdx, 0"); // failure flag tells codegen to box PHP false emitter.label("__rt_filesize_ret"); emitter.instruction(&format!("add rsp, {}", frame_size)); // release the temporary stat buffer frame emitter.instruction("pop rbp"); // restore the caller frame pointer @@ -339,11 +374,13 @@ fn emit_stat_linux_x86_64(emitter: &mut Emitter) { emitter.label_global("__rt_filemtime"); emit_linux_stat_call(emitter, frame_size); emitter.instruction("cmp eax, 0"); // test whether libc stat() succeeded before reading st_mtime - emitter.instruction("jne __rt_filemtime_fail"); // return zero when the stat call fails + emitter.instruction("jne __rt_filemtime_fail"); // php reports false when the path cannot be stat'ed emitter.instruction(&format!("mov rax, QWORD PTR [rsp + {}]", mtime_off)); // load st_mtime.tv_sec from the Linux stat buffer + emitter.instruction("mov rdx, 1"); // success flag for codegen-side int|false boxing emitter.instruction("jmp __rt_filemtime_ret"); // skip the failure path after reading the modification time emitter.label("__rt_filemtime_fail"); - emitter.instruction("mov rax, 0"); // return zero when the path could not be stated + emitter.instruction("mov rax, 0"); // stat failed: integer payload defaults to 0 + emitter.instruction("mov rdx, 0"); // failure flag tells codegen to box PHP false emitter.label("__rt_filemtime_ret"); emitter.instruction(&format!("add rsp, {}", frame_size)); // release the temporary stat buffer frame emitter.instruction("pop rbp"); // restore the caller frame pointer diff --git a/src/codegen_support/runtime/io/stat_array.rs b/src/codegen_support/runtime/io/stat_array.rs index 8b09bdcb80..af5363daac 100644 --- a/src/codegen_support/runtime/io/stat_array.rs +++ b/src/codegen_support/runtime/io/stat_array.rs @@ -192,17 +192,20 @@ pub fn emit_stat_array(emitter: &mut Emitter) { /// `emitter.target.arch == Arch::X86_64`. fn emit_stat_array_linux_x86_64(emitter: &mut Emitter) { let stat_buf = 144usize; - let mode_off = 24usize; + // The Windows C-symbol shims produce Elephc's portable stat layout rather + // than glibc's x86_64 structure. Keep Linux's native offsets unchanged. + let windows_layout = emitter.platform == crate::codegen_support::platform::Platform::Windows; + let mode_off = if windows_layout { emitter.platform.stat_mode_offset() } else { 24usize }; let size_off = 48usize; let atime_off = 72usize; let mtime_off = 88usize; let ctime_off = 104usize; let ino_off = 8usize; - let uid_off = 28usize; - let gid_off = 32usize; + let uid_off = if windows_layout { emitter.platform.stat_uid_offset() } else { 28usize }; + let gid_off = if windows_layout { emitter.platform.stat_gid_offset() } else { 32usize }; let dev_off = 0usize; - let rdev_off = 40usize; - let nlink_off = 16usize; + let rdev_off = if windows_layout { emitter.platform.stat_rdev_offset() } else { 40usize }; + let nlink_off = if windows_layout { emitter.platform.stat_nlink_offset() } else { 16usize }; let blksize_off = 56usize; let blocks_off = 64usize; diff --git a/src/codegen_support/runtime/io/stat_ext.rs b/src/codegen_support/runtime/io/stat_ext.rs index b860b6697d..6b3f0e726b 100644 --- a/src/codegen_support/runtime/io/stat_ext.rs +++ b/src/codegen_support/runtime/io/stat_ext.rs @@ -8,7 +8,7 @@ //! Key details: //! - I/O helpers bridge PHP strings, resources, descriptors, and libc calls while returning runtime arrays or pointer/length strings. -use crate::codegen_support::{emit::Emitter, platform::Arch}; +use crate::codegen_support::{emit::Emitter, platform::{Arch, Platform}}; use crate::codegen_support::abi; /// Emits the `__rt_filesize`, `__rt_filemtime` runtime helper assembly for stat ext. @@ -263,12 +263,15 @@ pub fn emit_stat_ext(emitter: &mut Emitter) { /// buffer in C ABI space rather than relying on a fixed syscall buffer layout. fn emit_stat_ext_linux_x86_64(emitter: &mut Emitter) { let frame_size = 144usize; - let mode_off = 24usize; + // The Windows `stat`/`lstat` C-symbol shims write the portable runtime + // layout, unlike glibc's x86_64 layout used by the native Linux path. + let windows_layout = emitter.platform == Platform::Windows; + let mode_off = if windows_layout { emitter.platform.stat_mode_offset() } else { 24usize }; let atime_off = 72usize; let ctime_off = 104usize; let ino_off = 8usize; - let uid_off = 28usize; - let gid_off = 32usize; + let uid_off = if windows_layout { emitter.platform.stat_uid_offset() } else { 28usize }; + let gid_off = if windows_layout { emitter.platform.stat_gid_offset() } else { 32usize }; // Reusable prologue/epilogue helpers for the libc-stat-based scalar getters. let stat_call = |emitter: &mut Emitter| { @@ -420,20 +423,52 @@ fn emit_stat_ext_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("ret"); // return the failure sentinel // -- is_executable -- + // Windows has no Unix exec bit. php-src asks GetBinaryTypeW and therefore + // rejects scripts and files that merely carry an executable-looking suffix. + // The Linux x86_64 path remains the libc access(X_OK) implementation. emitter.blank(); emitter.comment("--- runtime: is_executable ---"); emitter.label_global("__rt_is_executable"); - emitter.instruction("push rbp"); // preserve caller frame pointer - emitter.instruction("mov rbp, rsp"); // establish a stable frame base - emitter.instruction("call __rt_cstr"); // null-terminate the path - emitter.instruction("mov rdi, rax"); // first libc access() argument - emitter.instruction("mov rsi, 1"); // X_OK = 1 (execute permission check) - emitter.instruction("call access"); // libc access(path, X_OK) - emitter.instruction("cmp eax, 0"); // did libc access() return success as a C int? - emitter.instruction("sete al"); // boolean byte - emitter.instruction("movzx rax, al"); // widen to canonical integer result - emitter.instruction("pop rbp"); // restore caller frame pointer - emitter.instruction("ret"); // return predicate + if emitter.platform == Platform::Windows { + emitter.instruction("push rbp"); // preserve caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable frame base + emitter.instruction("sub rsp, 48"); // native shadow space plus result and owned-path locals + emitter.instruction("call __rt_cstr"); // materialize a NUL-terminated UTF-8 path + emitter.instruction("mov rdi, rax"); // pass the C path to strict UTF conversion + emitter.instruction("call __rt_win_utf8_to_utf16"); // allocate a Unicode path for GetBinaryTypeW + emitter.instruction("test rax, rax"); // conversion succeeded? + emitter.instruction("jz __rt_is_executable_no"); // invalid UTF-8 is not executable + emitter.instruction("mov QWORD PTR [rsp + 40], rax"); // retain the owned wide path across the native call + emitter.instruction("mov rcx, rax"); // lpApplicationName = wide path + emitter.instruction("lea rdx, [rsp + 32]"); // lpBinaryType = DWORD result storage + emitter.instruction("call GetBinaryTypeW"); // recognize an actual Windows binary image + emitter.instruction("mov DWORD PTR [rsp + 36], eax"); // preserve BOOL across path cleanup + emitter.instruction("mov rax, QWORD PTR [rsp + 40]"); // reload the owned wide path + emitter.instruction("call __rt_heap_free"); // release the conversion + emitter.instruction("cmp DWORD PTR [rsp + 36], 0"); // did Windows recognize a binary? + emitter.instruction("setne al"); // canonical boolean byte + emitter.instruction("movzx eax, al"); // widen to the runtime boolean representation + emitter.instruction("add rsp, 48"); // release native-call locals + emitter.instruction("pop rbp"); // restore caller frame pointer + emitter.instruction("ret"); // return predicate + emitter.label("__rt_is_executable_no"); + emitter.instruction("xor eax, eax"); // conversion failure is not executable + emitter.instruction("add rsp, 48"); // release native-call locals + emitter.instruction("pop rbp"); // restore caller frame pointer + emitter.instruction("ret"); // return false + } else { + emitter.instruction("push rbp"); // preserve caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable frame base + emitter.instruction("call __rt_cstr"); // null-terminate the path + emitter.instruction("mov rdi, rax"); // first libc access() argument + emitter.instruction("mov rsi, 1"); // X_OK = 1 (execute permission check) + emitter.instruction("call access"); // libc access(path, X_OK) + emitter.instruction("cmp eax, 0"); // did libc access() return success as a C int? + emitter.instruction("sete al"); // boolean byte + emitter.instruction("movzx rax, al"); // widen to canonical integer result + emitter.instruction("pop rbp"); // restore caller frame pointer + emitter.instruction("ret"); // return predicate + } // -- is_link -- emitter.blank(); diff --git a/src/codegen_support/runtime/io/stdout_write.rs b/src/codegen_support/runtime/io/stdout_write.rs index 133cbb0217..f6413fb623 100644 --- a/src/codegen_support/runtime/io/stdout_write.rs +++ b/src/codegen_support/runtime/io/stdout_write.rs @@ -80,7 +80,7 @@ pub fn emit_stdout_write(emitter: &mut Emitter, web: bool) { crate::codegen_support::abi::emit_symbol_address(emitter, "x9", &capture_symbol); emitter.instruction("ldrb w9, [x9]"); // load the low byte of the output-capture flag emitter.instruction("cbz x9, __rt_stdout_write_syscall"); // capture disabled — fall through to the plain write syscall - emitter.bl_c("elephc_web_write"); // capture enabled — append the bytes to the current request's response body (ptr=x0, len=x1) + emitter.emit_native_bridge_symbol_call("elephc_web_write", 2); // capture enabled — append the bytes to the current request's response body (ptr=x0, len=x1) emitter.instruction("b __rt_stdout_write_done"); // capture handled the bytes — skip the syscall path } @@ -96,7 +96,7 @@ pub fn emit_stdout_write(emitter: &mut Emitter, web: bool) { emitter.instruction("ret"); // return to caller } -/// Emits the x86_64 Linux variant of the `__rt_stdout_write` runtime helper. +/// Emits the x86_64 variant of the `__rt_stdout_write` runtime helper. /// /// Inputs: byte pointer in `rdi`, length in `rsi`. No result. Establishes an /// `rbp` frame (which leaves `rsp` 16-byte aligned for the capture branch's @@ -145,7 +145,7 @@ fn emit_stdout_write_x86_64(emitter: &mut Emitter, web: bool) { emitter.instruction("movzx r11d, BYTE PTR [r11]"); // load the low byte of the output-capture flag, zero-extended emitter.instruction("test r11d, r11d"); // is per-request output capture enabled? emitter.instruction("jz __rt_stdout_write_syscall"); // capture disabled — fall through to the plain write syscall - emitter.bl_c("elephc_web_write"); // capture enabled — append the bytes to the current request's response body (ptr=rdi, len=rsi) + emitter.emit_native_bridge_symbol_call("elephc_web_write", 2); // capture enabled — cross into the native bridge with Windows ABI staging when needed emitter.instruction("jmp __rt_stdout_write_done"); // capture handled the bytes — skip the syscall path } @@ -240,6 +240,16 @@ mod tests { assert!(linux_x86.contains("elephc_web_capture")); assert!(!linux_x86.contains("_elephc_web_capture")); assert!(linux_x86.contains("call elephc_web_write")); + + let windows_x86 = render(Platform::Windows, Arch::X86_64, true); + assert!(windows_x86.contains("elephc_web_capture")); + assert!(windows_x86.contains("lea r11, [rip + elephc_web_write]")); + assert!(windows_x86.contains("sub rsp, 32")); + assert!(windows_x86.contains("mov rcx, rdi")); + assert!(windows_x86.contains("mov rdx, rsi")); + assert!(windows_x86.contains("call r11")); + assert!(windows_x86.contains("add rsp, 32")); + assert!(!windows_x86.contains("call elephc_web_write")); } /// Verifies both web and non-web emissions keep the plain `write(1, …)` syscall diff --git a/src/codegen_support/runtime/io/stream_context_get_bool_option.rs b/src/codegen_support/runtime/io/stream_context_get_bool_option.rs new file mode 100644 index 0000000000..22037ff596 --- /dev/null +++ b/src/codegen_support/runtime/io/stream_context_get_bool_option.rs @@ -0,0 +1,182 @@ +//! Purpose: +//! Emits the scalar truthiness lookup shared by TLS stream-context consumers. +//! It composes the existing integer and string option helpers. +//! +//! Called from: +//! - HTTPS and `stream_socket_enable_crypto` policy lowering. +//! +//! Key details: +//! - Missing options leave the caller-provided default untouched. +//! - Integer and bool values use their numeric truthiness; strings are false +//! only when empty or exactly `"0"`, matching PHP scalar truthiness. + +use crate::codegen_support::{emit::Emitter, platform::Arch}; + +/// Emits `__rt_get_bool_context_option`. +/// +/// Input: AArch64 x0..x3 = wrapper/option pointer-length pairs, x4 = output address. +/// x86_64 rdi/rsi/rdx/rcx = pairs, r8 = output address. +/// Output: 1 when a supported scalar option was found, otherwise 0. +pub fn emit_get_bool_context_option(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_get_bool_context_option_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: get_bool_context_option ---"); + emitter.label_global("__rt_get_bool_context_option"); + + // Frame (96 bytes): saved input args at 0..32, string scratch at 40..48, + // saved frame pointer and return address at 80..88. + emitter.instruction("sub sp, sp, #96"); // allocate the scalar lookup frame + emitter.instruction("stp x29, x30, [sp, #80]"); // preserve the caller frame and return address + emitter.instruction("add x29, sp, #80"); // establish the helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // preserve the wrapper pointer + emitter.instruction("str x1, [sp, #8]"); // preserve the wrapper length + emitter.instruction("str x2, [sp, #16]"); // preserve the option pointer + emitter.instruction("str x3, [sp, #24]"); // preserve the option length + emitter.instruction("str x4, [sp, #32]"); // preserve the caller output address + + // -- prefer native integer/bool context values -- + emitter.instruction("bl __rt_get_int_context_option"); // resolve integer and bool values directly + emitter.instruction("cbz x0, __rt_gbco_string"); // fall back when no integer or bool value exists + emitter.instruction("ldr x9, [sp, #32]"); // reload the caller output address + emitter.instruction("ldr x10, [x9]"); // load the integer or bool payload + emitter.instruction("cmp x10, #0"); // PHP treats only integer zero as false + emitter.instruction("cset x10, ne"); // normalize every nonzero integer to true + emitter.instruction("str x10, [x9]"); // publish the normalized boolean + emitter.instruction("b __rt_gbco_done"); // return the integer helper hit + emitter.label("__rt_gbco_string"); + + // -- fall back to PHP string truthiness -- + emitter.instruction("ldr x0, [sp, #0]"); // reload the wrapper pointer + emitter.instruction("ldr x1, [sp, #8]"); // reload the wrapper length + emitter.instruction("ldr x2, [sp, #16]"); // reload the option pointer + emitter.instruction("ldr x3, [sp, #24]"); // reload the option length + emitter.instruction("add x4, sp, #40"); // pass the string-pointer scratch address + emitter.instruction("add x5, sp, #48"); // pass the string-length scratch address + emitter.instruction("bl __rt_get_string_context_option"); // resolve string values from the same context entry + emitter.instruction("cbz x0, __rt_gbco_done"); // preserve the caller default when the option is missing + emitter.instruction("ldr x9, [sp, #48]"); // load the resolved string length + emitter.instruction("cbz x9, __rt_gbco_false"); // the empty string is false + emitter.instruction("cmp x9, #1"); // only a one-byte string can be the special false value + emitter.instruction("b.ne __rt_gbco_true"); // every longer non-empty string is true + emitter.instruction("ldr x10, [sp, #40]"); // load the resolved string pointer + emitter.instruction("ldrb w11, [x10]"); // inspect the sole string byte + emitter.instruction("cmp w11, #48"); // compare with ASCII `0` + emitter.instruction("b.eq __rt_gbco_false"); // exactly `"0"` is false + + emitter.label("__rt_gbco_true"); + emitter.instruction("mov x9, #1"); // materialize PHP true + emitter.instruction("b __rt_gbco_store"); // store the normalized boolean + emitter.label("__rt_gbco_false"); + emitter.instruction("mov x9, #0"); // materialize PHP false + emitter.label("__rt_gbco_store"); + emitter.instruction("ldr x10, [sp, #32]"); // reload the caller output address + emitter.instruction("str x9, [x10]"); // publish the normalized boolean value + emitter.instruction("mov x0, #1"); // report a supported scalar hit + + emitter.label("__rt_gbco_done"); + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore the caller frame and return address + emitter.instruction("add sp, sp, #96"); // release the scalar lookup frame + emitter.instruction("ret"); // return the hit or miss status +} + +/// Emits the x86_64 implementation of `__rt_get_bool_context_option`. +fn emit_get_bool_context_option_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: get_bool_context_option ---"); + emitter.label_global("__rt_get_bool_context_option"); + + // rbp-relative frame: saved inputs at -8..-40 and string scratch at -48..-56. + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish the helper frame pointer + emitter.instruction("sub rsp, 64"); // reserve aligned input and string scratch storage + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the wrapper pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // preserve the wrapper length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // preserve the option pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // preserve the option length + emitter.instruction("mov QWORD PTR [rbp - 40], r8"); // preserve the caller output address + + // -- prefer native integer/bool context values -- + emitter.instruction("call __rt_get_int_context_option"); // resolve integer and bool values directly + emitter.instruction("test rax, rax"); // did the integer helper find a supported value? + emitter.instruction("jz __rt_gbco_string_x86"); // fall back when no integer or bool value exists + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the caller output address + emitter.instruction("cmp QWORD PTR [r10], 0"); // PHP treats only integer zero as false + emitter.instruction("xor r11d, r11d"); // clear the full normalization register + emitter.instruction("setne r11b"); // normalize every nonzero integer to true + emitter.instruction("mov QWORD PTR [r10], r11"); // publish the normalized boolean + emitter.instruction("mov eax, 1"); // preserve the integer helper hit result + emitter.instruction("jmp __rt_gbco_done_x86"); // return the normalized integer hit + emitter.label("__rt_gbco_string_x86"); + + // -- fall back to PHP string truthiness -- + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the wrapper pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload the wrapper length + emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // reload the option pointer + emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // reload the option length + emitter.instruction("lea r8, [rbp - 48]"); // pass the string-pointer scratch address + emitter.instruction("lea r9, [rbp - 56]"); // pass the string-length scratch address + emitter.instruction("call __rt_get_string_context_option"); // resolve string values from the same context entry + emitter.instruction("test rax, rax"); // did the string helper find a supported value? + emitter.instruction("jz __rt_gbco_done_x86"); // preserve the caller default when the option is missing + emitter.instruction("mov r10, QWORD PTR [rbp - 56]"); // load the resolved string length + emitter.instruction("test r10, r10"); // is the resolved string empty? + emitter.instruction("jz __rt_gbco_false_x86"); // the empty string is false + emitter.instruction("cmp r10, 1"); // only a one-byte string can be the special false value + emitter.instruction("jne __rt_gbco_true_x86"); // every longer non-empty string is true + emitter.instruction("mov r11, QWORD PTR [rbp - 48]"); // load the resolved string pointer + emitter.instruction("cmp BYTE PTR [r11], 48"); // compare the sole byte with ASCII `0` + emitter.instruction("je __rt_gbco_false_x86"); // exactly `"0"` is false + + emitter.label("__rt_gbco_true_x86"); + emitter.instruction("mov r10, 1"); // materialize PHP true + emitter.instruction("jmp __rt_gbco_store_x86"); // store the normalized boolean + emitter.label("__rt_gbco_false_x86"); + emitter.instruction("xor r10d, r10d"); // materialize PHP false + emitter.label("__rt_gbco_store_x86"); + emitter.instruction("mov r11, QWORD PTR [rbp - 40]"); // reload the caller output address + emitter.instruction("mov QWORD PTR [r11], r10"); // publish the normalized boolean value + emitter.instruction("mov eax, 1"); // report a supported scalar hit + + emitter.label("__rt_gbco_done_x86"); + emitter.instruction("add rsp, 64"); // release input and string scratch storage + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the hit or miss status +} + +#[cfg(test)] +mod tests { + use crate::codegen_support::platform::{Platform, Target}; + + use super::*; + + /// Verifies both backends compose typed lookup with PHP string truthiness. + #[test] + fn bool_context_lookup_preserves_php_scalar_truthiness() { + for target in [ + Target::new(Platform::MacOS, Arch::AArch64), + Target::new(Platform::Linux, Arch::X86_64), + ] { + let mut emitter = Emitter::new(target); + emit_get_bool_context_option(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("__rt_get_int_context_option")); + assert!(asm.contains("__rt_get_string_context_option")); + match target.arch { + Arch::AArch64 => { + assert!(asm.contains("cset x10, ne")); + assert!(asm.contains("cbz x9, __rt_gbco_false")); + assert!(asm.contains("cmp w11, #48")); + } + Arch::X86_64 => { + assert!(asm.contains("setne r11b")); + assert!(asm.contains("jz __rt_gbco_false_x86")); + assert!(asm.contains("cmp BYTE PTR [r11], 48")); + } + } + } + } +} diff --git a/src/codegen_support/runtime/io/stream_get_contents.rs b/src/codegen_support/runtime/io/stream_get_contents.rs index bca376bfce..a5ca322f20 100644 --- a/src/codegen_support/runtime/io/stream_get_contents.rs +++ b/src/codegen_support/runtime/io/stream_get_contents.rs @@ -44,6 +44,14 @@ pub fn emit_stream_get_contents(emitter: &mut Emitter) { emitter.instruction("add x12, x11, x10"); // compute the result start pointer emitter.instruction("str x12, [sp, #16]"); // save the result start pointer emitter.instruction("str xzr, [sp, #24]"); // initialize the running byte total to zero + // The accumulation buffer starts as the rest of the concat arena and moves to the + // heap the moment the stream outgrows it. Without that it kept copying chunks past + // the arena's 64 KiB end: the returned string was still right -- read back from + // the overflowed region -- while whatever followed the buffer was destroyed. + emitter.instruction("str x12, [sp, #48]"); // accumulation base, initially inside the arena + emitter.instruction("mov x13, #65536"); // the arena's fixed capacity + emitter.instruction("sub x13, x13, x10"); // what is left of it from the result start + emitter.instruction("str x13, [sp, #56]"); // accumulation capacity // -- read 4096-byte chunks through fread until EOF -- emitter.label("__rt_stream_get_contents_loop"); @@ -57,7 +65,18 @@ pub fn emit_stream_get_contents(emitter: &mut Emitter) { emitter.label("__rt_stream_get_contents_after_feof"); emitter.instruction("ldr x9, [sp, #24]"); // running result length emitter.instruction("ldr x12, [sp, #8]"); // result start offset + // While the accumulation still lives in the arena, pointing __rt_fread at the + // compact tail makes the copy below a self-copy and costs nothing. Once it has + // moved to the heap the arena is ours again from the result start, so each chunk + // is read into that scratch and copied out. + emitter.instruction("ldr x14, [sp, #48]"); // accumulation base + emit_symbol_address(emitter, "x15", "_concat_buf"); + emitter.instruction("sub x15, x14, x15"); // distance from the arena base + emitter.instruction("mov x16, #65536"); // the arena's fixed capacity + emitter.instruction("cmp x15, x16"); // is the accumulation still inside it? + emitter.instruction("b.hs __rt_stream_get_contents_scratch"); // heap-backed: read into the arena scratch emitter.instruction("add x12, x12, x9"); // compact append offset = start + total + emitter.label("__rt_stream_get_contents_scratch"); emit_symbol_address(emitter, "x13", "_concat_off"); emitter.instruction("str x12, [x13]"); // make __rt_fread append at the compact tail emitter.instruction("ldr x0, [sp, #0]"); // reload fd for __rt_fread @@ -66,11 +85,53 @@ pub fn emit_stream_get_contents(emitter: &mut Emitter) { emitter.instruction("cbz x2, __rt_stream_get_contents_release_done"); // empty read stops the read-all loop emitter.instruction("str x1, [sp, #32]"); // save chunk pointer across the copy emitter.instruction("str x2, [sp, #40]"); // save chunk length across the copy + // -- grow the accumulation buffer when this chunk would not fit -- + // Doubling, so a stream of n bytes costs O(n) copying rather than O(n**2). + emitter.instruction("ldr x9, [sp, #24]"); // running result length + emitter.instruction("ldr x10, [sp, #40]"); // this chunk's length + emitter.instruction("add x14, x9, x10"); // bytes the buffer must hold + emitter.instruction("ldr x13, [sp, #56]"); // current capacity + emitter.instruction("cmp x14, x13"); // does the chunk still fit? + emitter.instruction("b.ls __rt_sgc_grow_done"); // it fits: keep the current buffer + emitter.instruction("lsl x15, x13, #1"); // twice the current capacity + emitter.instruction("cmp x15, x14"); // is doubling already enough? + emitter.instruction("csel x15, x15, x14, hi"); // take whichever is larger + emitter.instruction("str x15, [sp, #56]"); // publish the new capacity + emitter.instruction("mov x0, x15"); // allocate the grown buffer + emitter.instruction("bl __rt_heap_alloc"); // x0 = the new accumulation base + emitter.instruction("mov x11, #1"); // heap kind 1 = owned elephc string + emitter.instruction("str x11, [x0, #-8]"); // stamp it: raw blocks are skipped by every decref + emitter.instruction("ldr x14, [sp, #48]"); // the old accumulation base + emitter.instruction("ldr x9, [sp, #24]"); // bytes accumulated so far + emitter.instruction("mov x12, #0"); // byte-copy index + emitter.label("__rt_sgc_grow_copy"); + emitter.instruction("cmp x12, x9"); // copied everything accumulated? + emitter.instruction("b.ge __rt_sgc_grow_copied"); // the move is complete + emitter.instruction("ldrb w11, [x14, x12]"); // load one accumulated byte + emitter.instruction("strb w11, [x0, x12]"); // place it in the grown buffer + emitter.instruction("add x12, x12, #1"); // advance the copy index + emitter.instruction("b __rt_sgc_grow_copy"); // continue the move + emitter.label("__rt_sgc_grow_copied"); + emitter.instruction("str x0, [sp, #48]"); // publish the new base before any call can clobber x0 + emit_symbol_address(emitter, "x13", "_concat_buf"); + emitter.instruction("sub x15, x14, x13"); // distance of the old base from the arena + emitter.instruction("mov x13, #65536"); // the arena's fixed capacity + emitter.instruction("cmp x15, x13"); // was the old buffer the arena itself? + emitter.instruction("b.lo __rt_sgc_grow_release_arena"); // yes: hand the arena space back + emitter.instruction("mov x0, x14"); // no: it was an earlier grown block + emitter.instruction("bl __rt_heap_free"); // release it + emitter.instruction("b __rt_sgc_grow_done"); // the move is finished + emitter.label("__rt_sgc_grow_release_arena"); + emitter.instruction("ldr x12, [sp, #8]"); // the result start offset + emit_symbol_address(emitter, "x13", "_concat_off"); + emitter.instruction("str x12, [x13]"); // the arena is free again from there + emitter.label("__rt_sgc_grow_done"); + + emitter.instruction("ldr x11, [sp, #48]"); // accumulation base emitter.instruction("ldr x9, [sp, #24]"); // reload running result length after __rt_fread - emit_symbol_address(emitter, "x11", "_concat_buf"); - emitter.instruction("ldr x12, [sp, #8]"); // result start offset - emitter.instruction("add x11, x11, x12"); // result base pointer - emitter.instruction("add x11, x11, x9"); // destination = result base + total + emitter.instruction("add x11, x11, x9"); // destination = accumulation base + total + emitter.instruction("ldr x1, [sp, #32]"); // reload the chunk pointer + emitter.instruction("ldr x2, [sp, #40]"); // reload the chunk length emitter.instruction("mov x12, #0"); // byte-copy index emitter.label("__rt_stream_get_contents_copy"); emitter.instruction("cmp x12, x2"); // copied this whole chunk? @@ -84,10 +145,20 @@ pub fn emit_stream_get_contents(emitter: &mut Emitter) { emitter.instruction("ldr x10, [sp, #40]"); // copied chunk length emitter.instruction("add x9, x9, x10"); // include the copied chunk in the total emitter.instruction("str x9, [sp, #24]"); // store the updated result length + // Only an arena-backed accumulation owns arena space past the result start; once + // it is heap-backed the cursor stays where the loop head put it, so the next + // chunk reuses the same scratch. + emitter.instruction("ldr x14, [sp, #48]"); // accumulation base + emit_symbol_address(emitter, "x13", "_concat_buf"); + emitter.instruction("sub x15, x14, x13"); // distance from the arena base + emitter.instruction("mov x13, #65536"); // the arena's fixed capacity + emitter.instruction("cmp x15, x13"); // is the accumulation still inside it? + emitter.instruction("b.hs __rt_stream_get_contents_tail_kept"); // heap-backed: leave the cursor alone emitter.instruction("ldr x12, [sp, #8]"); // result start offset emitter.instruction("add x12, x12, x9"); // compact tail offset after this chunk emit_symbol_address(emitter, "x13", "_concat_off"); emitter.instruction("str x12, [x13]"); // publish the compacted concat-buffer tail + emitter.label("__rt_stream_get_contents_tail_kept"); emitter.instruction("ldr x0, [sp, #32]"); // reload the chunk pointer emitter.instruction("bl __rt_decref_any"); // release owned wrapper/filter chunks; concat slices are ignored emitter.instruction("b __rt_stream_get_contents_loop"); // read the next chunk @@ -97,7 +168,11 @@ pub fn emit_stream_get_contents(emitter: &mut Emitter) { emitter.instruction("mov x0, x1"); // final empty chunk pointer emitter.instruction("bl __rt_decref_any"); // release it if it is heap-backed emitter.label("__rt_stream_get_contents_done"); - emitter.instruction("ldr x1, [sp, #16]"); // return the result start pointer + // The accumulation base, not the arena slot it started in: for a stream that + // outgrew the arena the result lives in an owned block. Like __rt_fread's, that + // block is never reclaimed -- the result is borrowed by contract, so no caller + // releases it. See the KNOWN GAP note in io/fread.rs. + emitter.instruction("ldr x1, [sp, #48]"); // return the accumulation base emitter.instruction("ldr x2, [sp, #24]"); // return the accumulated result length emitter.instruction("ldp x29, x30, [sp, #64]"); // restore frame pointer and return address emitter.instruction("add sp, sp, #80"); // release the helper frame @@ -214,6 +289,13 @@ fn emit_stream_get_contents_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("lea rax, [r11 + r10]"); // compute the result start pointer emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the result start pointer emitter.instruction("mov QWORD PTR [rbp - 32], 0"); // initialize the running byte total to zero + // Same accumulation contract as the AArch64 arm: start in what is left of the + // arena and move to the heap the moment the stream outgrows it, rather than + // copying chunks past the arena's 64 KiB end. + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // accumulation base, initially inside the arena + emitter.instruction("mov rax, 65536"); // the arena's fixed capacity + emitter.instruction("sub rax, r10"); // what is left of it from the result start + emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // accumulation capacity emitter.label("__rt_stream_get_contents_loop_x86"); emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the source file descriptor @@ -226,7 +308,16 @@ fn emit_stream_get_contents_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_stream_get_contents_after_feof_x86"); emitter.instruction("mov r8, QWORD PTR [rbp - 32]"); // running result length emitter.instruction("mov r11, QWORD PTR [rbp - 16]"); // result start offset + // Arena-backed: point __rt_fread at the compact tail so the copy below is a + // self-copy. Heap-backed: the arena is ours again from the result start, so the + // chunk is read into that scratch and copied out. + emitter.instruction("mov r9, QWORD PTR [rbp - 56]"); // accumulation base + abi::emit_symbol_address(emitter, "r10", "_concat_buf"); // the arena base + emitter.instruction("sub r9, r10"); // distance from the arena base + emitter.instruction("cmp r9, 65536"); // is the accumulation still inside it? + emitter.instruction("jae __rt_stream_get_contents_scratch_x86"); // heap-backed: read into the arena scratch emitter.instruction("add r11, r8"); // compact append offset = start + total + emitter.label("__rt_stream_get_contents_scratch_x86"); abi::emit_store_reg_to_symbol(emitter, "r11", "_concat_off", 0); // make __rt_fread append at the compact tail emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload fd for __rt_fread emitter.instruction("mov rsi, 4096"); // request one read-all chunk @@ -235,10 +326,48 @@ fn emit_stream_get_contents_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jz __rt_stream_get_contents_release_done_x86"); // empty read stops the read-all loop emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save chunk pointer across the copy emitter.instruction("mov QWORD PTR [rbp - 48], rdx"); // save chunk length across the copy + // -- grow the accumulation buffer when this chunk would not fit -- + // Doubling, so a stream of n bytes costs O(n) copying rather than O(n**2). + emitter.instruction("mov r8, QWORD PTR [rbp - 32]"); // running result length + emitter.instruction("add r8, QWORD PTR [rbp - 48]"); // bytes the buffer must hold + emitter.instruction("cmp r8, QWORD PTR [rbp - 64]"); // does the chunk still fit? + emitter.instruction("jbe __rt_sgc_grow_done_x86"); // it fits: keep the current buffer + emitter.instruction("mov rax, QWORD PTR [rbp - 64]"); // the current capacity + emitter.instruction("shl rax, 1"); // twice the current capacity + emitter.instruction("cmp rax, r8"); // is doubling already enough? + emitter.instruction("cmovb rax, r8"); // take whichever is larger + emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // publish the new capacity + emitter.instruction("call __rt_heap_alloc"); // rax = the new accumulation base + emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(1))); // owned-string kind word carrying the x86_64 heap magic + emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp it: raw blocks are skipped by every decref + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // the old accumulation base + emitter.instruction("mov r8, QWORD PTR [rbp - 32]"); // bytes accumulated so far + emitter.instruction("xor rcx, rcx"); // byte-copy index + emitter.label("__rt_sgc_grow_copy_x86"); + emitter.instruction("cmp rcx, r8"); // copied everything accumulated? + emitter.instruction("jge __rt_sgc_grow_copied_x86"); // the move is complete + emitter.instruction("mov r9b, BYTE PTR [r11 + rcx]"); // load one accumulated byte + emitter.instruction("mov BYTE PTR [rax + rcx], r9b"); // place it in the grown buffer + emitter.instruction("inc rcx"); // advance the copy index + emitter.instruction("jmp __rt_sgc_grow_copy_x86"); // continue the move + emitter.label("__rt_sgc_grow_copied_x86"); + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // publish the new base before any call can clobber rax + abi::emit_symbol_address(emitter, "r10", "_concat_buf"); // the arena base + emitter.instruction("mov r9, r11"); // copy the old base for the range test + emitter.instruction("sub r9, r10"); // distance of the old base from the arena + emitter.instruction("cmp r9, 65536"); // was the old buffer the arena itself? + emitter.instruction("jb __rt_sgc_grow_release_arena_x86"); // yes: hand the arena space back + emitter.instruction("mov rax, r11"); // no: it was an earlier grown block + emitter.instruction("call __rt_heap_free"); // release it + emitter.instruction("jmp __rt_sgc_grow_done_x86"); // the move is finished + emitter.label("__rt_sgc_grow_release_arena_x86"); + emitter.instruction("mov r11, QWORD PTR [rbp - 16]"); // the result start offset + abi::emit_store_reg_to_symbol(emitter, "r11", "_concat_off", 0); // the arena is free again from there + emitter.label("__rt_sgc_grow_done_x86"); + emitter.instruction("mov r8, QWORD PTR [rbp - 32]"); // reload running result length after __rt_fread - abi::emit_symbol_address(emitter, "r10", "_concat_buf"); // materialize the concat-buffer base - emitter.instruction("add r10, QWORD PTR [rbp - 16]"); // result base pointer - emitter.instruction("add r10, r8"); // destination = result base + total + emitter.instruction("mov r10, QWORD PTR [rbp - 56]"); // accumulation base + emitter.instruction("add r10, r8"); // destination = accumulation base + total emitter.instruction("mov r11, QWORD PTR [rbp - 40]"); // source chunk pointer emitter.instruction("xor rcx, rcx"); // byte-copy index emitter.label("__rt_stream_get_contents_copy_x86"); @@ -252,9 +381,17 @@ fn emit_stream_get_contents_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov r8, QWORD PTR [rbp - 32]"); // running result length before this chunk emitter.instruction("add r8, QWORD PTR [rbp - 48]"); // include the copied chunk in the total emitter.instruction("mov QWORD PTR [rbp - 32], r8"); // store the updated result length + // Only an arena-backed accumulation owns arena space past the result start; once + // it is heap-backed the cursor stays where the loop head put it. + emitter.instruction("mov r9, QWORD PTR [rbp - 56]"); // accumulation base + abi::emit_symbol_address(emitter, "r10", "_concat_buf"); // the arena base + emitter.instruction("sub r9, r10"); // distance from the arena base + emitter.instruction("cmp r9, 65536"); // is the accumulation still inside it? + emitter.instruction("jae __rt_stream_get_contents_tail_kept_x86"); // heap-backed: leave the cursor alone emitter.instruction("mov r11, QWORD PTR [rbp - 16]"); // result start offset emitter.instruction("add r11, r8"); // compact tail offset after this chunk abi::emit_store_reg_to_symbol(emitter, "r11", "_concat_off", 0); // publish the compacted concat-buffer tail + emitter.label("__rt_stream_get_contents_tail_kept_x86"); emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the chunk pointer emitter.instruction("call __rt_decref_any"); // release owned wrapper/filter chunks; concat slices are ignored emitter.instruction("jmp __rt_stream_get_contents_loop_x86"); // read the next chunk @@ -262,7 +399,9 @@ fn emit_stream_get_contents_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_stream_get_contents_release_done_x86"); emitter.instruction("call __rt_decref_any"); // release the empty chunk if it is heap-backed emitter.label("__rt_stream_get_contents_done_x86"); - emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // return the result start pointer + // The accumulation base, not the arena slot it started in: for a stream that + // outgrew the arena the result lives in an owned block. + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // return the accumulation base emitter.instruction("mov rdx, QWORD PTR [rbp - 32]"); // return the accumulated result length emitter.instruction("add rsp, 64"); // release the helper locals emitter.instruction("pop rbp"); // restore the caller frame pointer diff --git a/src/codegen_support/runtime/io/stream_get_line.rs b/src/codegen_support/runtime/io/stream_get_line.rs index d9f33eec5a..f48941dee6 100644 --- a/src/codegen_support/runtime/io/stream_get_line.rs +++ b/src/codegen_support/runtime/io/stream_get_line.rs @@ -11,7 +11,7 @@ //! (which is consumed and stripped). EOF/read failure sets `_eof_flags`. use crate::codegen_support::abi::emit_symbol_address; -use crate::codegen_support::{emit::Emitter, platform::Arch}; +use crate::codegen_support::{emit::Emitter, platform::{Arch, Platform}}; use crate::codegen_support::abi; /// stream_get_line: read up to a length or an ending delimiter from a stream. @@ -217,6 +217,11 @@ fn emit_stream_get_line_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("cmp rax, QWORD PTR [rbp - 16]"); // reached the byte budget? emitter.instruction("jge __rt_stream_get_line_done_x86"); // stop at the maximum length + if emitter.target.platform == crate::codegen_support::platform::Platform::Windows { + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // restore the opaque descriptor before resetting timeout metadata + emitter.instruction("call __rt_win_stream_clear_timed_out"); // each new line-read operation starts with timed_out=false + } + abi::emit_load_symbol_to_reg(emitter, "r9", "_concat_off", 0); // current concat-buffer offset abi::emit_symbol_address(emitter, "r10", "_concat_buf"); // concat-buffer base address emitter.instruction("lea rsi, [r10 + r9]"); // single-byte write pointer @@ -270,6 +275,13 @@ fn emit_stream_get_line_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov r10d, DWORD PTR [rax]"); // load the thread-local errno value emitter.instruction("cmp r10d, 11"); // is this EAGAIN/EWOULDBLOCK from a nonblocking fd? emitter.instruction("je __rt_stream_get_line_done_x86"); // transient nonblocking miss returns without setting EOF + if emitter.target.platform == crate::codegen_support::platform::Platform::Windows { + emitter.instruction("cmp r10d, 110"); // ETIMEDOUT is retryable stream timeout, not EOF + emitter.instruction("jne __rt_stream_get_line_eof_x86"); // only non-timeout failures set EOF + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // restore the opaque stream descriptor for timeout metadata + emitter.instruction("call __rt_win_stream_mark_timed_out"); // record timed_out without touching EOF state + emitter.instruction("jmp __rt_stream_get_line_done_x86"); // return the partial line without EOF + } // -- user-wrapper line read: feof-gated stream_read into _user_wrapper_drain_buf // (a SEPARATE buffer from _concat_buf, which each __rt_fread result may @@ -326,8 +338,13 @@ fn emit_stream_get_line_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_stream_get_line_eof_x86"); emitter.instruction("mov r9, QWORD PTR [rbp - 8]"); // reload the file descriptor + if emitter.target.platform == Platform::Windows { + emitter.instruction("mov rdi, r9"); // pass the opaque Windows descriptor to the slot registry + emitter.instruction("call __rt_win_stream_slot"); // obtain a bounded EOF-table slot + emitter.instruction("mov r9, rax"); // table indexing uses the compact slot, never a raw SOCKET + } abi::emit_symbol_address(emitter, "r10", "_eof_flags"); // eof-flag table base address - emitter.instruction("mov BYTE PTR [r10 + r9], 1"); // record EOF for this descriptor + emitter.instruction("mov BYTE PTR [r10 + r9], 1"); // record EOF for this compact stream slot emitter.label("__rt_stream_get_line_done_x86"); emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // return the result start pointer diff --git a/src/codegen_support/runtime/io/stream_get_meta_data.rs b/src/codegen_support/runtime/io/stream_get_meta_data.rs index cd8b00648a..deee6e91e4 100644 --- a/src/codegen_support/runtime/io/stream_get_meta_data.rs +++ b/src/codegen_support/runtime/io/stream_get_meta_data.rs @@ -12,7 +12,7 @@ //! - `wrapper_type` is reported as `"plainfile"` and `uri` as the empty string: //! elephc does not track per-resource open paths. -use crate::codegen_support::{emit::Emitter, platform::Arch}; +use crate::codegen_support::{emit::Emitter, platform::{Arch, Platform}}; use crate::codegen_support::abi; /// stream_get_meta_data: build the metadata hash for a stream descriptor. @@ -186,7 +186,7 @@ fn emit_stream_get_meta_data_linux_x86_64(emitter: &mut Emitter) { // Frame (rbp-relative): [-8]=fd [-16]=hash [-24]=seekable [-32]=blocked // [-40]=eof [-48]=mode_ptr [-56]=mode_len - // [-64]=stype_ptr [-72]=stype_len + // [-64]=stype_ptr [-72]=stype_len [-80]=timed_out emitter.instruction("push rbp"); // preserve the caller frame pointer emitter.instruction("mov rbp, rsp"); // establish the helper frame pointer emitter.instruction("sub rsp, 80"); // reserve the metadata spill slots @@ -247,12 +247,23 @@ fn emit_stream_get_meta_data_linux_x86_64(emitter: &mut Emitter) { // -- end-of-file flag from the _eof_flags table -- emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the stream descriptor + if emitter.target.platform == Platform::Windows { + emitter.instruction("call __rt_win_stream_slot"); // map opaque SOCKET/CRT descriptor to its bounded state slot + emitter.instruction("mov rdi, rax"); // fixed EOF table is indexed only by the compact slot + } abi::emit_symbol_address(emitter, "r10", "_eof_flags"); // address of the EOF flag table - emitter.instruction("movzx r11, BYTE PTR [r10 + rdi]"); // load _eof_flags[fd] + emitter.instruction("movzx r11, BYTE PTR [r10 + rdi]"); // load _eof_flags[slot] without raw Windows-handle indexing emitter.instruction("test r11, r11"); // has end-of-file been observed? emitter.instruction("setne r11b"); // eof = 1 when the flag byte is set emitter.instruction("movzx r11, r11b"); // widen the eof flag to a full word emitter.instruction("mov QWORD PTR [rbp - 40], r11"); // save the eof flag + if emitter.target.platform == crate::codegen_support::platform::Platform::Windows { + abi::emit_symbol_address(emitter, "r10", "_win_stream_timed_out"); // timeout state table base + emitter.instruction("movzx r11, BYTE PTR [r10 + rdi]"); // load the compact slot's retryable timeout flag + emitter.instruction("mov QWORD PTR [rbp - 80], r11"); // preserve timed_out while constructing metadata + } else { + emitter.instruction("mov QWORD PTR [rbp - 80], 0"); // non-Windows timeout tracking is not slot-backed + } // -- create the metadata hash (capacity 16, value type = mixed) -- emitter.instruction("mov rdi, 16"); // initial capacity @@ -260,7 +271,7 @@ fn emit_stream_get_meta_data_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("call __rt_hash_new"); // allocate the hash; rax = hash pointer emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // save the hash pointer - emit_set_bool_const_x86(emitter, "_meta_key_timed_out", 9, 0); + emit_set_bool_slot_x86(emitter, "_meta_key_timed_out", 9, 80); emit_set_bool_slot_x86(emitter, "_meta_key_blocked", 7, 32); emit_set_bool_slot_x86(emitter, "_meta_key_eof", 3, 40); emit_set_int_const_x86(emitter, "_meta_key_unread_bytes", 12); @@ -286,14 +297,6 @@ fn emit_hash_put_x86(emitter: &mut Emitter, key_sym: &str, key_len: i64) { emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // persist any post-grow hash pointer } -/// Emits the set bool const x86 stream runtime helper. -fn emit_set_bool_const_x86(emitter: &mut Emitter, key_sym: &str, key_len: i64, value: i64) { - emitter.instruction(&format!("mov rcx, {}", value)); // value_lo = boolean payload - emitter.instruction("xor r8d, r8d"); // value_hi unused for booleans - emitter.instruction("mov r9, 3"); // value tag = bool - emit_hash_put_x86(emitter, key_sym, key_len); -} - /// Emits the set bool slot x86 stream runtime helper. fn emit_set_bool_slot_x86(emitter: &mut Emitter, key_sym: &str, key_len: i64, slot: i64) { emitter.instruction(&format!("mov rcx, QWORD PTR [rbp - {}]", slot)); // value_lo = computed boolean diff --git a/src/codegen_support/runtime/io/stream_isatty.rs b/src/codegen_support/runtime/io/stream_isatty.rs index 8141da9a62..1dea0ffa89 100644 --- a/src/codegen_support/runtime/io/stream_isatty.rs +++ b/src/codegen_support/runtime/io/stream_isatty.rs @@ -1,20 +1,29 @@ //! Purpose: //! Emits the `__rt_stream_isatty` runtime helper assembly for stream_isatty. -//! Probes whether a file descriptor refers to a terminal via an `ioctl` request. +//! Probes whether a file descriptor refers to a terminal through the native +//! platform terminal API. //! //! Called from: //! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::io`. //! //! Key details: -//! - Uses the platform terminal-attributes `ioctl` request (`TIOCGETA` on macOS, -//! `TCGETS` on Linux); a successful call means the descriptor is a terminal. +//! - Uses `GetConsoleMode` on Windows after `_get_osfhandle`; Unix targets use +//! the platform terminal-attributes `ioctl` request. -use crate::codegen_support::{emit::Emitter, platform::Arch}; +use crate::codegen_support::{ + emit::Emitter, + platform::{Arch, Platform}, +}; /// stream_isatty: report whether a file descriptor is connected to a terminal. /// Input: x0=fd /// Output: x0=1 if the descriptor is a terminal, 0 otherwise pub fn emit_stream_isatty(emitter: &mut Emitter) { + if emitter.platform == Platform::Windows { + emit_stream_isatty_windows_x86_64(emitter); + return; + } + if emitter.target.arch == Arch::X86_64 { emit_stream_isatty_linux_x86_64(emitter); return; @@ -44,6 +53,38 @@ pub fn emit_stream_isatty(emitter: &mut Emitter) { emitter.instruction("ret"); // return the terminal flag to the caller } +/// Emits the Windows x86_64 `stream_isatty` helper through the CRT descriptor +/// table and the Win32 console-mode probe. +/// +/// The generated runtime enters with its normal SysV-style descriptor in `rdi`, +/// while `_get_osfhandle` and `GetConsoleMode` use the MSx64 ABI. A CRT lookup +/// failure or a redirected handle makes the helper return false without ever +/// treating the descriptor as a Winsock socket. +fn emit_stream_isatty_windows_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: stream_isatty ---"); + emitter.label_global("__rt_stream_isatty"); + + // -- resolve the CRT descriptor, then ask the console itself for its mode -- + emitter.instruction("sub rsp, 56"); // reserve MSx64 shadow space and an aligned console-mode DWORD + emitter.instruction("mov ecx, edi"); // CRT descriptor for _get_osfhandle's MSx64 first argument + emitter.instruction("call _get_osfhandle"); // recover the native HANDLE or INVALID_HANDLE_VALUE + emitter.instruction("cmp rax, -1"); // invalid CRT descriptor or raw socket? + emitter.instruction("je .Lstream_isatty_windows_false"); // neither is a terminal stream + emitter.instruction("mov rcx, rax"); // hConsoleHandle = converted CRT descriptor + emitter.instruction("lea rdx, [rsp + 40]"); // lpMode = stack-local DWORD outside shadow space + emitter.instruction("call GetConsoleMode"); // only console handles expose a readable mode + emitter.instruction("test eax, eax"); // Win32 BOOL success means an attached console + emitter.instruction("setne al"); // al = 1 for a terminal and 0 for a redirected handle + emitter.instruction("movzx eax, al"); // zero-extend the PHP boolean result + emitter.instruction("add rsp, 56"); // release shadow space and mode local + emitter.instruction("ret"); // return the terminal flag + emitter.label(".Lstream_isatty_windows_false"); + emitter.instruction("xor eax, eax"); // invalid descriptors and raw sockets are not terminals + emitter.instruction("add rsp, 56"); // release shadow space and mode local + emitter.instruction("ret"); // return false +} + /// Emits the Linux x86_64 stream runtime helper for stream isatty. fn emit_stream_isatty_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); @@ -66,3 +107,21 @@ fn emit_stream_isatty_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("movzx rax, al"); // zero-extend the terminal flag into the full result register emitter.instruction("ret"); // return the terminal flag to the caller } + +#[cfg(test)] +mod tests { + use super::*; + use crate::codegen::platform::Target; + + /// Verifies Windows probes console handles instead of issuing a socket ioctl. + #[test] + fn windows_uses_get_console_mode() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_stream_isatty(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("call _get_osfhandle")); + assert!(asm.contains("call GetConsoleMode")); + assert!(!asm.contains("ioctl")); + assert!(!asm.contains("ioctlsocket")); + } +} diff --git a/src/codegen_support/runtime/io/stream_select.rs b/src/codegen_support/runtime/io/stream_select.rs index f98a37db51..eb3a8174f9 100644 --- a/src/codegen_support/runtime/io/stream_select.rs +++ b/src/codegen_support/runtime/io/stream_select.rs @@ -246,6 +246,17 @@ fn emit_stream_select_linux_x86_64(emitter: &mut Emitter) { emit_build_fdset_x86(emitter, arr_off, fds_off, suffix); } + // Winsock's select() rejects three empty fd_sets with WSAEINVAL, while + // POSIX pselect() treats the same zero-timeout probe as a successful wait + // with no ready descriptors. User wrappers without stream_cast() are + // deliberately excluded above and can therefore leave all sets empty. + if emitter.platform == crate::codegen_support::platform::Platform::Windows { + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // load the read descriptor bitmap + emitter.instruction("or rax, QWORD PTR [rbp - 40]"); // merge the write descriptor bitmap + emitter.instruction("or rax, QWORD PTR [rbp - 48]"); // merge the exception descriptor bitmap + emitter.instruction("jz __rt_stream_select_empty_x86"); // bypass Winsock when every set is empty + } + // -- pselect6(nfds=64, read, write, except, timeout, sigmask) -- emitter.instruction("mov edi, 64"); // nfds: examine descriptors 0..63 emitter.instruction("lea rsi, [rbp - 32]"); // read descriptor bitmap pointer @@ -256,6 +267,12 @@ fn emit_stream_select_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov eax, 270"); // Linux x86_64 syscall 270 = pselect6 emitter.instruction("syscall"); // wait for descriptor readiness emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // save the ready descriptor count + if emitter.platform == crate::codegen_support::platform::Platform::Windows { + emitter.instruction("jmp __rt_stream_select_compact_x86"); // continue with the post-select bitmaps + emitter.label("__rt_stream_select_empty_x86"); + emitter.instruction("mov QWORD PTR [rbp - 72], 0"); // POSIX-compatible empty-set result + emitter.label("__rt_stream_select_compact_x86"); + } for (arr_off, fds_off, suffix) in [(8, 32, "r"), (16, 40, "w"), (24, 48, "e")] { emit_compact_x86(emitter, arr_off, fds_off, suffix); @@ -310,9 +327,22 @@ fn emit_build_fdset_x86(emitter: &mut Emitter, arr_off: i64, fds_off: i64, suffi emitter.label(&cast_skip_l); emitter.instruction("cmp rdx, 0"); // is the descriptor negative? emitter.instruction(&format!("jl {}", next_l)); // skip descriptors that cannot be set - emitter.instruction("cmp rdx, 64"); // is the descriptor outside the word-0 range? - emitter.instruction(&format!("jae {}", next_l)); // skip out-of-range descriptors - emitter.instruction("mov rcx, rdx"); // descriptor position for the shift count + if emitter.platform == crate::codegen_support::platform::Platform::Windows { + let table = match suffix { + "r" => "_win_select_read_handles", + "w" => "_win_select_write_handles", + _ => "_win_select_except_handles", + }; + emitter.instruction("cmp rsi, 64"); // Windows bridge has one selectable slot per array position + emitter.instruction(&format!("jae {}", next_l)); // skip positions outside the bridge table + emitter.instruction(&format!("lea rax, [rip + {}]", table)); // raw SOCKET lookup table for pselect6 + emitter.instruction("mov QWORD PTR [rax + rsi * 8], rdx"); // associate compact bit position with the full-width SOCKET + emitter.instruction("mov rcx, rsi"); // bitmap position is the array slot, not the opaque SOCKET value + } else { + emitter.instruction("cmp rdx, 64"); // is the descriptor outside the word-0 range? + emitter.instruction(&format!("jae {}", next_l)); // skip out-of-range descriptors + emitter.instruction("mov rcx, rdx"); // descriptor position for the shift count + } emitter.instruction("mov rax, 1"); // bit seed for the descriptor emitter.instruction("shl rax, cl"); // shift the bit into the descriptor position emitter.instruction(&format!("or QWORD PTR [rbp - {}], rax", fds_off)); // set the descriptor bit @@ -372,9 +402,15 @@ fn emit_compact_x86(emitter: &mut Emitter, arr_off: i64, fds_off: i64, suffix: & emitter.label(&cast_skip_l); emitter.instruction("cmp rdx, 0"); // is the descriptor negative? emitter.instruction(&format!("jl {}", next_l)); // drop descriptors that were never set - emitter.instruction("cmp rdx, 64"); // is the descriptor outside the word-0 range? - emitter.instruction(&format!("jae {}", next_l)); // drop out-of-range descriptors - emitter.instruction("mov rcx, rdx"); // descriptor position for the shift count + if emitter.platform == crate::codegen_support::platform::Platform::Windows { + emitter.instruction("cmp rsi, 64"); // ready bitmap is keyed by original array position + emitter.instruction(&format!("jae {}", next_l)); // drop out-of-range source positions + emitter.instruction("mov rcx, rsi"); // test the slot bit returned by the Win32 bridge + } else { + emitter.instruction("cmp rdx, 64"); // is the descriptor outside the word-0 range? + emitter.instruction(&format!("jae {}", next_l)); // drop out-of-range descriptors + emitter.instruction("mov rcx, rdx"); // descriptor position for the shift count + } emitter.instruction("mov rax, r8"); // copy the bitmap word for testing emitter.instruction("shr rax, cl"); // shift the descriptor bit to position 0 emitter.instruction("and rax, 1"); // isolate the descriptor's ready bit diff --git a/src/codegen_support/runtime/io/stream_set_blocking.rs b/src/codegen_support/runtime/io/stream_set_blocking.rs index 450e31ed6a..8f4488e43c 100644 --- a/src/codegen_support/runtime/io/stream_set_blocking.rs +++ b/src/codegen_support/runtime/io/stream_set_blocking.rs @@ -17,7 +17,7 @@ use crate::codegen_support::{emit::Emitter, platform::Arch}; /// Output: x0 = 1 on success, 0 on failure pub fn emit_stream_set_blocking(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { - emit_stream_set_blocking_linux_x86_64(emitter); + emit_stream_set_blocking_x86_64(emitter); return; } @@ -72,8 +72,12 @@ pub fn emit_stream_set_blocking(emitter: &mut Emitter) { emitter.instruction("ret"); // return the failure result } -/// Emits the Linux x86_64 stream runtime helper for stream set blocking. -fn emit_stream_set_blocking_linux_x86_64(emitter: &mut Emitter) { +/// Emits the x86_64 stream runtime helper for toggling descriptor blocking mode. +/// +/// Windows post-processing rewrites both fcntl syscalls to `__rt_sys_fcntl`, +/// which applies `FIONBIO` to sockets and retains cache-only semantics for CRT +/// descriptors such as the standard streams. +fn emit_stream_set_blocking_x86_64(emitter: &mut Emitter) { let plat = emitter.platform; emitter.blank(); emitter.comment("--- runtime: stream_set_blocking ---"); @@ -124,3 +128,32 @@ fn emit_stream_set_blocking_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the failure result } + +#[cfg(test)] +mod tests { + use super::*; + use crate::codegen_support::platform::{Platform, Target, transform_for_windows}; + + /// Verifies Windows rewrites both x86_64 fcntl operations through the shim + /// that applies `FIONBIO` to sockets while preserving CRT descriptor state. + #[test] + fn test_stream_set_blocking_windows_uses_fcntl_shim() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_stream_set_blocking(&mut emitter); + let asm = transform_for_windows(&emitter.output()); + assert_eq!(asm.matches("call __rt_sys_fcntl").count(), 2); + assert!(asm.contains("mov r9d, 2048"), "missing Windows O_NONBLOCK mask"); + assert!(!asm.contains("syscall"), "Windows output must not retain raw fcntl syscalls"); + } + + /// Verifies that the Linux x86_64 dispatch is unaffected by the Windows + /// branch and still routes through the `fcntl(F_GETFL/F_SETFL)` path. + #[test] + fn test_stream_set_blocking_linux_still_uses_fcntl() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_stream_set_blocking(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains(".globl __rt_stream_set_blocking\n"), "missing global label"); + assert!(asm.contains("mov eax, 72"), "Linux helper must still use the fcntl syscall number"); + } +} diff --git a/src/codegen_support/runtime/io/stream_set_timeout.rs b/src/codegen_support/runtime/io/stream_set_timeout.rs index 0713fd471f..0538e92cb3 100644 --- a/src/codegen_support/runtime/io/stream_set_timeout.rs +++ b/src/codegen_support/runtime/io/stream_set_timeout.rs @@ -1,6 +1,6 @@ //! Purpose: -//! Emits the `__rt_stream_set_timeout` runtime helper, which applies a read -//! timeout to a socket descriptor through `setsockopt(SO_RCVTIMEO)`. +//! Emits the `__rt_stream_set_timeout` runtime helper, which applies read and +//! write timeouts to a socket descriptor through `setsockopt`. //! //! Called from: //! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::io`. @@ -14,7 +14,7 @@ use crate::codegen_support::{emit::Emitter, platform::Arch}; -/// stream_set_timeout: set the receive timeout on a socket descriptor. +/// Emits the native stream timeout setter for the active target ABI. /// Input: AArch64 x0 = fd, x1 = seconds, x2 = microseconds /// x86_64 rdi = fd, rsi = seconds, rdx = microseconds /// Output: 1 on success, 0 on failure @@ -55,6 +55,10 @@ pub fn emit_stream_set_timeout(emitter: &mut Emitter) { /// Emits the Linux x86_64 stream runtime helper for stream set timeout. fn emit_stream_set_timeout_linux_x86_64(emitter: &mut Emitter) { + if emitter.target.platform == crate::codegen_support::platform::Platform::Windows { + emit_stream_set_timeout_windows_x86_64(emitter); + return; + } emitter.blank(); emitter.comment("--- runtime: stream_set_timeout ---"); emitter.label_global("__rt_stream_set_timeout"); @@ -81,3 +85,40 @@ fn emit_stream_set_timeout_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("add rsp, 16"); // release the timeval emitter.instruction("ret"); // return the boolean result } + +/// Emits the WinSock receive/send timeout setter and clears stale timeout metadata. +fn emit_stream_set_timeout_windows_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: stream_set_timeout ---"); + emitter.label_global("__rt_stream_set_timeout"); + emitter.instruction("sub rsp, 40"); // preserve a POSIX timeval and opaque descriptor across two shim calls + emitter.instruction("mov QWORD PTR [rsp + 24], rdi"); // preserve the full-width SOCKET across both option updates + emitter.instruction("mov QWORD PTR [rsp], rsi"); // timeval.tv_sec = PHP seconds + emitter.instruction("mov QWORD PTR [rsp + 8], rdx"); // timeval.tv_usec = PHP microseconds + emitter.instruction("mov rsi, 1"); // POSIX SOL_SOCKET, translated by the WinSock shim + emitter.instruction("mov rdx, 20"); // POSIX SO_RCVTIMEO, translated by the WinSock shim + emitter.instruction("mov r10, rsp"); // pass the POSIX timeval to the shim for millisecond conversion + emitter.instruction("mov r8, 16"); // sizeof(timeval) + emitter.instruction("call __rt_sys_setsockopt"); // configure receive timeout through the audited WinSock ABI shim + emitter.instruction("test rax, rax"); // shim returns zero on success and negative on failure + emitter.instruction("jnz __rt_stream_set_timeout_fail_win"); // receiving timeout must succeed before reporting true + emitter.instruction("mov rdi, QWORD PTR [rsp + 24]"); // restore the opaque SOCKET for the send timeout update + emitter.instruction("mov rsi, 1"); // POSIX SOL_SOCKET, translated by the WinSock shim + emitter.instruction("mov rdx, 21"); // POSIX SO_SNDTIMEO, translated by the WinSock shim + emitter.instruction("mov r10, rsp"); // reuse the POSIX timeval for millisecond conversion + emitter.instruction("mov r8, 16"); // sizeof(timeval) + emitter.instruction("call __rt_sys_setsockopt"); // configure send timeout through the audited WinSock ABI shim + emitter.instruction("test rax, rax"); // both socket options must succeed + emitter.instruction("jnz __rt_stream_set_timeout_fail_win"); // partial configuration reports false + emitter.instruction("mov rdi, QWORD PTR [rsp + 24]"); // resolve this stream's compact metadata slot + emitter.instruction("call __rt_win_stream_slot"); // opaque SOCKETs must never index metadata directly + crate::codegen_support::abi::emit_symbol_address(emitter, "r10", "_win_stream_timed_out"); // timeout state table base + emitter.instruction("mov BYTE PTR [r10 + rax], 0"); // a new timeout configuration clears stale timeout state + emitter.instruction("mov eax, 1"); // both socket options succeeded + emitter.instruction("add rsp, 40"); // release the timeout frame + emitter.instruction("ret"); // return PHP true + emitter.label("__rt_stream_set_timeout_fail_win"); + emitter.instruction("xor eax, eax"); // either socket-option failure reports PHP false + emitter.instruction("add rsp, 40"); // release the timeout frame + emitter.instruction("ret"); // return PHP false +} diff --git a/src/codegen_support/runtime/io/stream_socket_accept.rs b/src/codegen_support/runtime/io/stream_socket_accept.rs index 861fc2ea22..2fa91177ed 100644 --- a/src/codegen_support/runtime/io/stream_socket_accept.rs +++ b/src/codegen_support/runtime/io/stream_socket_accept.rs @@ -33,7 +33,7 @@ fn family_byte_offset(platform: Platform) -> i64 { match platform { Platform::MacOS => 1, Platform::Linux => 0, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 0, // Windows WinSock sockaddr puts the 16-bit family at offset 0, same as Linux } } diff --git a/src/codegen_support/runtime/io/stream_socket_get_name.rs b/src/codegen_support/runtime/io/stream_socket_get_name.rs index 753ae8ba2d..11e7966143 100644 --- a/src/codegen_support/runtime/io/stream_socket_get_name.rs +++ b/src/codegen_support/runtime/io/stream_socket_get_name.rs @@ -30,7 +30,7 @@ fn family_byte_offset(platform: Platform) -> u32 { match platform { Platform::MacOS => 1, Platform::Linux => 0, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 0, // Windows WinSock sockaddr puts the 16-bit family at offset 0, same as Linux } } diff --git a/src/codegen_support/runtime/io/stream_socket_pair.rs b/src/codegen_support/runtime/io/stream_socket_pair.rs index 1d2cf7b1b8..973efb0a05 100644 --- a/src/codegen_support/runtime/io/stream_socket_pair.rs +++ b/src/codegen_support/runtime/io/stream_socket_pair.rs @@ -14,7 +14,7 @@ //! as PHP `false`, matching PHP's `array|false` contract for the //! domains the kernel refuses (typically `STREAM_PF_INET`). -use crate::codegen_support::{emit::Emitter, platform::Arch}; +use crate::codegen_support::{emit::Emitter, platform::{Arch, Platform}}; /// stream_socket_pair: create a connected pair of socket descriptors. /// Input: AArch64 x0 = domain, x1 = type, x2 = protocol @@ -84,10 +84,18 @@ fn emit_stream_socket_pair_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov esi, 8"); // element size = 8 bytes emitter.instruction("call __rt_array_new"); // allocate the result array, rax = pointer emitter.instruction("mov rdi, rax"); // array pointer argument - emitter.instruction("mov esi, DWORD PTR [rbp - 16]"); // sv[0] = first socket descriptor + if emitter.target.platform == Platform::Windows { + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // sv[0] = full-width Windows SOCKET + } else { + emitter.instruction("mov esi, DWORD PTR [rbp - 16]"); // sv[0] = first POSIX descriptor + } emitter.instruction("call __rt_array_push_int"); // push the first descriptor, rax = array emitter.instruction("mov rdi, rax"); // array pointer argument - emitter.instruction("mov esi, DWORD PTR [rbp - 12]"); // sv[1] = second socket descriptor + if emitter.target.platform == Platform::Windows { + emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // sv[1] = full-width Windows SOCKET + } else { + emitter.instruction("mov esi, DWORD PTR [rbp - 12]"); // sv[1] = second POSIX descriptor + } emitter.instruction("call __rt_array_push_int"); // push the second descriptor, rax = array emitter.instruction("add rsp, 16"); // release the frame emitter.instruction("pop rbp"); // restore the caller frame pointer diff --git a/src/codegen_support/runtime/io/stream_socket_recvfrom.rs b/src/codegen_support/runtime/io/stream_socket_recvfrom.rs index 74383521a0..99b46d5058 100644 --- a/src/codegen_support/runtime/io/stream_socket_recvfrom.rs +++ b/src/codegen_support/runtime/io/stream_socket_recvfrom.rs @@ -32,7 +32,7 @@ fn family_byte_offset(platform: Platform) -> i64 { match platform { Platform::MacOS => 1, Platform::Linux => 0, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 0, // Windows WinSock sockaddr puts the 16-bit family at offset 0, same as Linux } } diff --git a/src/codegen_support/runtime/io/stream_socket_server.rs b/src/codegen_support/runtime/io/stream_socket_server.rs index 3d536313c8..99c0ac4ce1 100644 --- a/src/codegen_support/runtime/io/stream_socket_server.rs +++ b/src/codegen_support/runtime/io/stream_socket_server.rs @@ -157,6 +157,31 @@ pub fn emit_stream_socket_server(emitter: &mut Emitter) { emitter.instruction("b __rt_stream_socket_server_fail"); // socket() failed emitter.label("__rt_stream_socket_server_sock_ok"); emitter.instruction("str x0, [sp, #32]"); // save the socket descriptor + + // -- setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &1, 4) -- + // php enables SO_REUSEADDR by default on every listener and treats the + // socket.so_reuseaddr context option purely as an opt-out + // (main/streams/xp_socket.c). Without it a server cannot rebind its own port + // while the previous socket is still in TIME_WAIT. The IPv6 helper already + // does this; the option value is staged in the sockaddr area, which is only + // built further down, so no extra frame slot is needed. + { + let (sol_socket, so_reuseaddr): (i64, i64) = match plat { + Platform::MacOS => (0xffff, 4), + Platform::Linux => (1, 2), + Platform::Windows => (1, 2), + }; + emitter.instruction("mov w9, #1"); // SO_REUSEADDR option value = 1 + emitter.instruction("str w9, [sp, #40]"); // stage it in the not-yet-built sockaddr area + emitter.instruction("ldr x0, [sp, #32]"); // reload the socket descriptor + emitter.instruction(&format!("mov x1, #{}", sol_socket)); // level: SOL_SOCKET + emitter.instruction(&format!("mov x2, #{}", so_reuseaddr)); // name: SO_REUSEADDR + emitter.instruction("add x3, sp, #40"); // pointer to the option value + emitter.instruction("mov x4, #4"); // option length = sizeof(int) + emitter.syscall(105); // best-effort: a failure must not fail the bind + } + + emitter.instruction("ldr x0, [sp, #32]"); // reload the descriptor for the options helper emitter.instruction("bl __rt_apply_socket_server_opts"); // apply so_reuseport before bind (best-effort) emitter.instruction("ldr x0, [sp, #32]"); // reload the descriptor (helper clobbers x0) @@ -363,7 +388,22 @@ fn emit_stream_socket_server_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("test rax, rax"); // did socket() fail? emitter.instruction("js __rt_stream_socket_server_fail_x86"); // socket() failed emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the socket descriptor - emitter.instruction("mov rdi, rax"); // pass the fd to the options helper + + // -- setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &1, 4) -- + // See the AArch64 arm: php enables this by default on every listener. The + // POSIX level/optname pair is emitted verbatim because the Windows shim + // translates them to their Winsock values itself (shims_net.rs), and the + // option value is staged in the sockaddr area, which is built further down. + emitter.instruction("mov DWORD PTR [rbp - 40], 1"); // SO_REUSEADDR option value = 1 + emitter.instruction("mov rdi, rax"); // fd + emitter.instruction("mov rsi, 1"); // level: POSIX SOL_SOCKET + emitter.instruction("mov rdx, 2"); // name: POSIX SO_REUSEADDR + emitter.instruction("lea r10, [rbp - 40]"); // pointer to the option value + emitter.instruction("mov r8, 4"); // option length = sizeof(int) + emitter.instruction("mov eax, 54"); // Linux x86_64 syscall 54 = setsockopt + emitter.instruction("syscall"); // best-effort: a failure must not fail the bind + + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload the fd for the options helper emitter.instruction("call __rt_apply_socket_server_opts"); // apply so_reuseport before bind (best-effort) emitter.instruction("mov WORD PTR [rbp - 40], 2"); // Linux sin_family = AF_INET diff --git a/src/codegen_support/runtime/io/stream_socket_server_v6.rs b/src/codegen_support/runtime/io/stream_socket_server_v6.rs index 595589b465..4dfd76b7dc 100644 --- a/src/codegen_support/runtime/io/stream_socket_server_v6.rs +++ b/src/codegen_support/runtime/io/stream_socket_server_v6.rs @@ -167,7 +167,7 @@ pub fn emit_stream_socket_server_v6(emitter: &mut Emitter) { let (sol_socket, so_reuseaddr): (i64, i64) = match plat { Platform::MacOS => (0xffff, 4), Platform::Linux => (1, 2), - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => (1, 15), }; emitter.instruction("mov w11, #1"); // SO_REUSEADDR option value = 1 emitter.instruction("str w11, [sp, #84]"); // stash the option value in stack scratch diff --git a/src/codegen_support/runtime/io/streams_ext.rs b/src/codegen_support/runtime/io/streams_ext.rs index bf3f4a9627..7aa473b995 100644 --- a/src/codegen_support/runtime/io/streams_ext.rs +++ b/src/codegen_support/runtime/io/streams_ext.rs @@ -16,6 +16,10 @@ //! while preserving the `LOCK_NB` flag bit. //! - `__rt_tmpfile` returns the raw fd in x0/rax (-1 on failure); the codegen //! wrapper boxes it as resource/false via `__rt_mixed_from_value`. +//! - Unix `__rt_tmpfile` routes `mkstemp` through `Emitter::emit_call_c` and +//! immediately unlinks the path. Windows instead uses the system temp +//! directory plus `GetTempFileNameW` and reopens with +//! `FILE_FLAG_DELETE_ON_CLOSE`, matching php-src's close-time deletion. use crate::codegen_support::{ abi, @@ -225,12 +229,12 @@ pub fn emit_streams_ext(emitter: &mut Emitter) { let errno_func = match emitter.platform { Platform::MacOS => "__error", Platform::Linux => "__errno_location", - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => "__errno_location", // Windows shims against msvcrt errno }; let would_block_errno = match emitter.platform { Platform::MacOS => 35, Platform::Linux => 11, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 11, // EAGAIN — msvcrt uses the POSIX value via the shim }; emitter.bl_c(errno_func); // fetch thread-local errno storage after flock() failure emitter.instruction("ldr w9, [x0]"); // load errno value set by libc flock() @@ -250,41 +254,59 @@ pub fn emit_streams_ext(emitter: &mut Emitter) { // sp+ 0 : 32-byte template buffer (more than enough for /tmp/elephc-XXXXXX) // sp+32 : x29 / x30 // ================================================================ - let tmpl_buf = 32usize; - let tmpl_save = tmpl_buf; - let tmpl_frame = tmpl_buf + 16; + emitter.blank(); emitter.raw(" .p2align 2"); // ensure 4-byte alignment for the next runtime helper emitter.comment("--- runtime: tmpfile ---"); emitter.label_global("__rt_tmpfile"); - emitter.instruction(&format!("sub sp, sp, #{}", tmpl_frame)); // allocate frame + template buffer - emitter.instruction(&format!("stp x29, x30, [sp, #{}]", tmpl_save)); // save frame pointer and return address - emitter.instruction(&format!("add x29, sp, #{}", tmpl_save)); // establish new frame pointer - abi::emit_symbol_address(emitter, "x9", "_tmpfile_template"); // load page of the template literal - emitter.instruction("ldp x10, x11, [x9]"); // load 16 bytes of the template - emitter.instruction("stp x10, x11, [sp]"); // copy first 16 bytes onto the stack template - emitter.instruction("ldr x10, [x9, #16]"); // load the remaining bytes (≤ 8) of the template - emitter.instruction("str x10, [sp, #16]"); // copy the trailing bytes onto the stack template - - emitter.instruction("add x0, sp, #0"); // mkstemp template argument + emitter.instruction("sub sp, sp, #16"); // frame for the nested helper and libc calls + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address + emitter.instruction("mov x29, sp"); // establish the helper frame pointer + emitter.instruction("sub sp, sp, #1040"); // path buffer (1024 bytes) plus the fd spill slot + emitter.instruction("bl __rt_php_temp_dir"); // x1 = directory pointer, x2 = directory length + emitter.instruction("cmp x2, #1008"); // room left for "/elephc-XXXXXX" and its terminator? + emitter.instruction("b.ge __rt_tmpfile_fail"); // an over-long directory fails rather than truncating + emitter.instruction("mov x11, sp"); // path buffer base + emitter.instruction("mov x9, #0"); // path write index + emitter.label("__rt_tmpfile_dir"); + emitter.instruction("cmp x9, x2"); // copied the whole directory? + emitter.instruction("b.ge __rt_tmpfile_dir_done"); // directory copied + emitter.instruction("ldrb w10, [x1, x9]"); // load a directory byte + emitter.instruction("strb w10, [x11, x9]"); // append it to the path + emitter.instruction("add x9, x9, #1"); // advance the write index + emitter.instruction("b __rt_tmpfile_dir"); // continue copying the directory + emitter.label("__rt_tmpfile_dir_done"); + abi::emit_symbol_address(emitter, "x12", "_tmpfile_suffix"); // "/elephc-XXXXXX" plus its C terminator + emitter.instruction("mov x13, #0"); // suffix read index + emitter.label("__rt_tmpfile_suffix"); + emitter.instruction("ldrb w10, [x12, x13]"); // load a suffix byte + emitter.instruction("strb w10, [x11, x9]"); // append it, terminator included + emitter.instruction("cbz w10, __rt_tmpfile_ready"); // the terminator completes the template + emitter.instruction("add x9, x9, #1"); // advance the write index + emitter.instruction("add x13, x13, #1"); // advance the suffix read index + emitter.instruction("b __rt_tmpfile_suffix"); // continue copying the suffix + emitter.label("__rt_tmpfile_ready"); + emitter.instruction("mov x0, x11"); // mkstemp template argument emitter.bl_c("mkstemp"); // libc mkstemp() → fd (or -1) emitter.instruction("cmp w0, #0"); // did mkstemp return a negative C int? emitter.instruction("b.lt __rt_tmpfile_fail"); // mkstemp failed emitter.instruction("sxtw x0, w0"); // normalize the C int fd into the runtime's 64-bit descriptor value - emitter.instruction("str x0, [sp, #24]"); // preserve fd on the stack across the unlink call (x9–x15 are caller-saved) - emitter.instruction("add x0, sp, #0"); // unlink path argument (the now-resolved template) + emitter.instruction("str x0, [sp, #1024]"); // preserve fd across the unlink call + emitter.instruction("mov x0, sp"); // unlink path argument (the now-resolved template) emitter.bl_c("unlink"); // libc unlink — file auto-deletes when fd closes - emitter.instruction("ldr x0, [sp, #24]"); // reload fd as the return value + emitter.instruction("ldr x0, [sp, #1024]"); // reload fd as the return value abi::emit_symbol_address(emitter, "x9", "_eof_flags"); emitter.instruction("strb wzr, [x9, x0]"); // clear stale EOF state for the temporary descriptor - emitter.instruction(&format!("ldp x29, x30, [sp, #{}]", tmpl_save)); // restore frame pointer and return address - emitter.instruction(&format!("add sp, sp, #{}", tmpl_frame)); // deallocate frame + emitter.instruction("add sp, sp, #1040"); // release the path buffer + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the frame emitter.instruction("ret"); // return fd emitter.label("__rt_tmpfile_fail"); emitter.instruction("mov x0, #-1"); // tmpfile failure sentinel - emitter.instruction(&format!("ldp x29, x30, [sp, #{}]", tmpl_save)); // restore frame pointer and return address (failure path) - emitter.instruction(&format!("add sp, sp, #{}", tmpl_frame)); // deallocate frame (failure path) + emitter.instruction("add sp, sp, #1040"); // release the path buffer (failure path) + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address (failure path) + emitter.instruction("add sp, sp, #16"); // release the frame (failure path) emitter.instruction("ret"); // return -1 } @@ -366,6 +388,10 @@ fn emit_streams_ext_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [rbp - 16], 0"); // total bytes copied = 0 emitter.label("__rt_fpassthru_loop_x86"); + if emitter.target.platform == crate::codegen_support::platform::Platform::Windows { + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // restore the opaque descriptor before resetting timeout metadata + emitter.instruction("call __rt_win_stream_clear_timed_out"); // each new passthru read starts with timed_out=false + } emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // fd emitter.instruction(&format!("lea rsi, [rbp - {}]", buf_size + 16)); // buffer emitter.instruction(&format!("mov rdx, {}", buf_size)); // count @@ -381,17 +407,39 @@ fn emit_streams_ext_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_fpassthru_done_x86"); emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload fd so feof() observes that passthru reached EOF + if emitter.target.platform == Platform::Windows { + emitter.instruction("mov rdi, r10"); // pass the opaque Windows descriptor to the slot registry + emitter.instruction("call __rt_win_stream_slot"); // obtain a bounded EOF-table slot + emitter.instruction("mov r10, rax"); // table indexing uses the compact slot, never a raw SOCKET + } abi::emit_symbol_address(emitter, "r11", "_eof_flags"); // materialize the eof-flag table for fpassthru completion - emitter.instruction("mov BYTE PTR [r11 + r10], 1"); // set _eof_flags[fd] after consuming the stream + emitter.instruction("mov BYTE PTR [r11 + r10], 1"); // set EOF on the compact stream slot after consuming the stream emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // return total emitter.instruction(&format!("add rsp, {}", buf_size + 16)); // release frame emitter.instruction("pop rbp"); // restore caller frame pointer emitter.instruction("ret"); // return total emitter.label("__rt_fpassthru_read_error_x86"); + if emitter.target.platform == crate::codegen_support::platform::Platform::Windows { + emitter.instruction("call __errno_location"); // inspect the POSIX-mapped Winsock failure before setting EOF + emitter.instruction("cmp DWORD PTR [rax], 110"); // ETIMEDOUT is a retryable stream timeout, not EOF + emitter.instruction("jne __rt_fpassthru_error_eof_x86"); // preserve legacy EOF behavior for non-timeout failures + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // restore the opaque stream descriptor for timeout metadata + emitter.instruction("call __rt_win_stream_mark_timed_out"); // expose the timeout without changing EOF state + emitter.instruction("mov rax, -1"); // retain fpassthru's failure sentinel after a timeout + emitter.instruction(&format!("add rsp, {}", buf_size + 16)); // release the passthru frame without touching EOF + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the timeout failure sentinel + emitter.label("__rt_fpassthru_error_eof_x86"); + } emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload fd so feof() observes the exhausted error state + if emitter.target.platform == Platform::Windows { + emitter.instruction("mov rdi, r10"); // pass the opaque Windows descriptor to the slot registry + emitter.instruction("call __rt_win_stream_slot"); // obtain a bounded EOF-table slot + emitter.instruction("mov r10, rax"); // table indexing uses the compact slot, never a raw SOCKET + } abi::emit_symbol_address(emitter, "r11", "_eof_flags"); // materialize the eof-flag table after fpassthru read failure - emitter.instruction("mov BYTE PTR [r11 + r10], 1"); // set _eof_flags[fd] after a failed read + emitter.instruction("mov BYTE PTR [r11 + r10], 1"); // set EOF on the compact stream slot after a failed read emitter.instruction("mov rax, -1"); // read failure sentinel, matching PHP's -1 byte count emitter.instruction(&format!("add rsp, {}", buf_size + 16)); // release frame emitter.instruction("pop rbp"); // restore caller frame pointer @@ -430,38 +478,181 @@ fn emit_streams_ext_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore caller frame pointer emitter.instruction("ret"); // return predicate + if emitter.target.platform == Platform::Windows { + emit_tmpfile_windows_x86_64(emitter); + return; + } + // -- tmpfile -- emitter.blank(); emitter.comment("--- runtime: tmpfile ---"); emitter.label_global("__rt_tmpfile"); emitter.instruction("push rbp"); // preserve caller frame pointer emitter.instruction("mov rbp, rsp"); // establish frame - emitter.instruction("sub rsp, 48"); // reserve template buffer plus fd spill slot - abi::emit_symbol_address(emitter, "rsi", "_tmpfile_template"); // source pointer - emitter.instruction("mov rax, QWORD PTR [rsi]"); // load first 8 bytes - emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // store first 8 bytes - emitter.instruction("mov rax, QWORD PTR [rsi + 8]"); // load next 8 bytes - emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // store next 8 bytes - emitter.instruction("mov rax, QWORD PTR [rsi + 16]"); // load remainder - emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // store remainder - emitter.instruction("lea rdi, [rbp - 32]"); // mkstemp template arg - emitter.instruction("call mkstemp"); // libc mkstemp + emitter.instruction("sub rsp, 1056"); // path buffer (1024 bytes) plus the fd spill slot + abi::emit_call_label(emitter, "__rt_php_temp_dir"); // rax = directory pointer, rdx = directory length + emitter.instruction("cmp rdx, 1008"); // room left for "/elephc-XXXXXX" and its terminator? + emitter.instruction("jge __rt_tmpfile_fail_x86"); // an over-long directory fails rather than truncating + emitter.instruction("xor rcx, rcx"); // path write index + emitter.label("__rt_tmpfile_dir_x86"); + emitter.instruction("cmp rcx, rdx"); // copied the whole directory? + emitter.instruction("jge __rt_tmpfile_dir_done_x86"); // directory copied + emitter.instruction("mov r8b, BYTE PTR [rax + rcx]"); // load a directory byte + emitter.instruction("mov BYTE PTR [rbp + rcx - 1040], r8b"); // append it to the path + emitter.instruction("inc rcx"); // advance the write index + emitter.instruction("jmp __rt_tmpfile_dir_x86"); // continue copying the directory + emitter.label("__rt_tmpfile_dir_done_x86"); + abi::emit_symbol_address(emitter, "r9", "_tmpfile_suffix"); // "/elephc-XXXXXX" plus its C terminator + emitter.instruction("xor r10, r10"); // suffix read index + emitter.label("__rt_tmpfile_suffix_x86"); + emitter.instruction("mov r8b, BYTE PTR [r9 + r10]"); // load a suffix byte + emitter.instruction("mov BYTE PTR [rbp + rcx - 1040], r8b"); // append it, terminator included + emitter.instruction("test r8b, r8b"); // was that the terminator? + emitter.instruction("je __rt_tmpfile_ready_x86"); // the terminator completes the template + emitter.instruction("inc rcx"); // advance the write index + emitter.instruction("inc r10"); // advance the suffix read index + emitter.instruction("jmp __rt_tmpfile_suffix_x86"); // continue copying the suffix + emitter.label("__rt_tmpfile_ready_x86"); + emitter.instruction("lea rdi, [rbp - 1040]"); // mkstemp template arg + emitter.emit_call_c("mkstemp"); // Windows: exclusive CreateFileW-backed temporary-file shim emitter.instruction("cmp eax, 0"); // did mkstemp return a negative C int? emitter.instruction("jl __rt_tmpfile_fail_x86"); // mkstemp failed emitter.instruction("cdqe"); // normalize the C int fd into the runtime's 64-bit descriptor value - emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // preserve fd across unlink - emitter.instruction("lea rdi, [rbp - 32]"); // unlink path + emitter.instruction("mov QWORD PTR [rbp - 1048], rax"); // preserve fd across unlink + emitter.instruction("lea rdi, [rbp - 1040]"); // unlink path emitter.instruction("call unlink"); // libc unlink — file auto-deletes on close - emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // return fd - abi::emit_symbol_address(emitter, "r10", "_eof_flags"); // materialize the eof-flag table for the temporary descriptor - emitter.instruction("mov BYTE PTR [r10 + rax], 0"); // clear stale EOF state before returning the descriptor - emitter.instruction("add rsp, 48"); // release frame + emitter.instruction("mov rax, QWORD PTR [rbp - 1048]"); // return fd + if emitter.target.platform == Platform::Windows { + emitter.instruction("mov rdi, rax"); // pass temporary CRT descriptor to the slot registry + emitter.instruction("call __rt_win_stream_slot"); // obtain a bounded EOF-table slot + emitter.instruction("mov r11, rax"); // retain compact slot for state initialization + abi::emit_symbol_address(emitter, "r10", "_eof_flags"); // materialize the eof-flag table for the temporary descriptor slot + emitter.instruction("mov BYTE PTR [r10 + r11], 0"); // clear stale EOF state without raw descriptor indexing + emitter.instruction("mov rax, QWORD PTR [rbp - 1048]"); // return the original temporary CRT descriptor + } else { + abi::emit_symbol_address(emitter, "r10", "_eof_flags"); // materialize the eof-flag table for the temporary descriptor + emitter.instruction("mov BYTE PTR [r10 + rax], 0"); // clear stale EOF state before returning the descriptor + } + emitter.instruction("add rsp, 1056"); // release frame emitter.instruction("pop rbp"); // restore caller frame pointer emitter.instruction("ret"); // return fd emitter.label("__rt_tmpfile_fail_x86"); emitter.instruction("mov rax, -1"); // failure sentinel - emitter.instruction("add rsp, 48"); // release frame (failure path) + emitter.instruction("add rsp, 1056"); // release frame (failure path) emitter.instruction("pop rbp"); // restore caller frame pointer (failure path) emitter.instruction("ret"); // return -1 } + +/// Emits the Windows x86_64 `tmpfile()` path using the configured system temp +/// directory and a delete-on-close Win32 handle adopted by the CRT. +fn emit_tmpfile_windows_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: tmpfile (Windows delete-on-close) ---"); + emitter.label_global("__rt_tmpfile"); + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable frame for wide path buffers + emitter.instruction("sub rsp, 8320"); // Win64 call area below two php-src-sized UTF-16 path buffers and spill slots + emitter.instruction("mov DWORD PTR [rbp - 32], 0x006c0065"); // UTF-16 prefix bytes for "el" + emitter.instruction("mov DWORD PTR [rbp - 28], 0x00000070"); // UTF-16 prefix tail "p" plus terminator + emitter.instruction("mov ecx, 2048"); // PHP_WIN32_IOUTIL_MAXPATHLEN UTF-16 code units + emitter.instruction("lea rdx, [rbp - 4128]"); // writable php-src-sized temp-directory buffer + emitter.instruction("call GetTempPathW"); // resolve TMP/TEMP or the Windows directory fallback + emitter.instruction("test eax, eax"); // did Windows return a temp directory? + emitter.instruction("jz __rt_tmpfile_native_fail_win"); // publish GetLastError and return false + emitter.instruction("cmp eax, 2048"); // did the configured path exceed the php-src-sized buffer? + emitter.instruction("jae __rt_tmpfile_range_fail_win"); // reject truncation with ERANGE + emitter.instruction("lea rcx, [rbp - 4128]"); // existing system temp directory + emitter.instruction("lea rdx, [rbp - 32]"); // three-character UTF-16 prefix + emitter.instruction("xor r8d, r8d"); // let Windows choose a unique numeric suffix + emitter.instruction("lea r9, [rbp - 8224]"); // receive the created temporary path + emitter.instruction("call GetTempFileNameW"); // create a unique file in the configured temp directory + emitter.instruction("test eax, eax"); // did unique-file creation succeed? + emitter.instruction("jz __rt_tmpfile_native_fail_win"); // publish GetLastError and return false + + // -- reopen the named file with delete-on-close ownership -- + emitter.instruction("lea rcx, [rbp - 8224]"); // unique UTF-16 temporary path + emitter.instruction("mov rdx, 0xC0010000"); // GENERIC_READ | GENERIC_WRITE | DELETE + emitter.instruction("mov r8d, 7"); // share read, write, and delete while the stream is alive + emitter.instruction("xor r9d, r9d"); // default security and non-inheritable handle + emitter.instruction("mov QWORD PTR [rsp + 32], 3"); // OPEN_EXISTING file created by GetTempFileNameW + emitter.instruction("mov QWORD PTR [rsp + 40], 0x04000100"); // FILE_FLAG_DELETE_ON_CLOSE | FILE_ATTRIBUTE_TEMPORARY + emitter.instruction("mov QWORD PTR [rsp + 48], 0"); // no template handle + emitter.instruction("call CreateFileW"); // acquire the stream handle with close-time deletion + emitter.instruction("cmp rax, -1"); // INVALID_HANDLE_VALUE? + emitter.instruction("je __rt_tmpfile_open_fail_win"); // remove the named file and publish the native error + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // preserve the owned Win32 handle until CRT adoption + emitter.instruction("mov rcx, rax"); // handle transferred to _open_osfhandle on success + emitter.instruction("mov edx, 0x8002"); // _O_BINARY | _O_RDWR + emitter.instruction("call _open_osfhandle"); // expose the delete-on-close handle as a CRT descriptor + emitter.instruction("cmp eax, -1"); // did CRT descriptor allocation fail? + emitter.instruction("je __rt_tmpfile_crt_fail_win"); // close the still-owned handle and report EMFILE + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // preserve the CRT descriptor across slot lookup + emitter.instruction("mov rdi, rax"); // opaque descriptor for bounded stream state + emitter.instruction("call __rt_win_stream_slot"); // allocate or recover the compact stream-state slot + emitter.instruction("mov r11, rax"); // retain the slot while materializing the EOF table + abi::emit_symbol_address(emitter, "r10", "_eof_flags"); // bounded EOF-state table + emitter.instruction("mov BYTE PTR [r10 + r11], 0"); // a fresh temporary stream starts before EOF + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // return the adopted CRT descriptor + emitter.instruction("add rsp, 8320"); // release wide buffers and Win32 call staging + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the writable temporary resource descriptor + + emitter.label("__rt_tmpfile_open_fail_win"); + emitter.instruction("call GetLastError"); // preserve CreateFileW failure before cleanup + emitter.instruction("mov DWORD PTR [rbp - 24], eax"); // native open error + emitter.instruction("lea rcx, [rbp - 8224]"); // named file created by GetTempFileNameW + emitter.instruction("call DeleteFileW"); // avoid leaking a failed temporary-file path + emitter.instruction("mov eax, DWORD PTR [rbp - 24]"); // restore the original CreateFileW error + emitter.instruction("jmp __rt_tmpfile_translate_fail_win"); // publish the preserved error + + emitter.label("__rt_tmpfile_crt_fail_win"); + emitter.instruction("mov rcx, QWORD PTR [rbp - 16]"); // handle remains owned when CRT adoption fails + emitter.instruction("call CloseHandle"); // close and trigger FILE_FLAG_DELETE_ON_CLOSE + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 24"); // EMFILE: CRT descriptor table exhausted + emitter.instruction("jmp __rt_tmpfile_fail_win"); // return the failure sentinel + + emitter.label("__rt_tmpfile_native_fail_win"); + emitter.instruction("call GetLastError"); // obtain the failed temp-path API error + emitter.label("__rt_tmpfile_translate_fail_win"); + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // retain the native diagnostic code + emitter.instruction("call __rt_win32_errno_from_code"); // map the Win32 error into POSIX errno space + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish the temporary-file failure + emitter.instruction("jmp __rt_tmpfile_fail_win"); // return false + + emitter.label("__rt_tmpfile_range_fail_win"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 34"); // ERANGE: configured temp path exceeds MAX_PATH + emitter.label("__rt_tmpfile_fail_win"); + emitter.instruction("mov rax, -1"); // tmpfile failure sentinel + emitter.instruction("add rsp, 8320"); // release wide buffers and Win32 call staging + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return false through the builtin boxing path +} + +#[cfg(test)] +mod tests { + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::*; + + /// Verifies Windows temp files use the configured system directory and + /// delete-on-close ownership instead of the Unix `/tmp` unlink strategy. + #[test] + fn windows_tmpfile_uses_system_temp_and_delete_on_close() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_streams_ext(&mut emitter); + let asm = emitter.output(); + let section = asm + .split("__rt_tmpfile:\n") + .nth(1) + .expect("missing Windows tmpfile helper"); + assert!(section.contains("call GetTempPathW")); + assert!(section.contains("call GetTempFileNameW")); + assert!(section.contains("sub rsp, 8320")); + assert!(section.contains("mov QWORD PTR [rsp + 40], 0x04000100")); + assert!(section.contains("call _open_osfhandle")); + assert!(section.contains("cmp eax, -1")); + assert!(!section.contains("call __rt_sys_mkstemp")); + assert!(!section.contains("call unlink")); + } +} diff --git a/src/codegen_support/runtime/io/tempnam.rs b/src/codegen_support/runtime/io/tempnam.rs index 41f2d590ca..908c064002 100644 --- a/src/codegen_support/runtime/io/tempnam.rs +++ b/src/codegen_support/runtime/io/tempnam.rs @@ -7,10 +7,14 @@ //! //! Key details: //! - I/O helpers bridge PHP strings, resources, descriptors, and libc calls while returning runtime arrays or pointer/length strings. +//! - The x86_64 `mkstemp` call site routes through `Emitter::emit_call_c`. On +//! Windows, `__rt_sys_mkstemp` rewrites the trailing `XXXXXX`, creates the +//! path atomically through `CreateFileW(CREATE_NEW)`, and returns a CRT +//! descriptor. Other targets continue to use libc. -use crate::codegen_support::{emit::Emitter, platform::Arch}; use crate::codegen_support::abi; - +use crate::codegen_support::runtime::data::TEMPNAM_FALLBACK_NOTICE; +use crate::codegen_support::{emit::Emitter, platform::{Arch, Platform}}; /// Emits the `__rt_tempnam` runtime helper for creating a unique temporary filename. /// @@ -38,7 +42,34 @@ pub fn emit_tempnam(emitter: &mut Emitter) { emitter.instruction("stp x1, x2, [sp, #0]"); // save dir ptr and len emitter.instruction("stp x3, x4, [sp, #16]"); // save prefix ptr and len + // -- match PHP's basename(prefix) and 63-byte prefix limit -- + emitter.instruction("mov x12, x3"); // scan the original prefix without allocating an intermediate basename + emitter.instruction("mov x13, x4"); // retain the remaining candidate basename byte length + emitter.label("__rt_tempnam_prefix_scan"); + emitter.instruction("cbz x13, __rt_tempnam_prefix_limit"); // finish once every input prefix byte has been inspected + emitter.instruction("ldrb w14, [x12]"); // inspect the next prefix byte for a path separator + emitter.instruction("cmp w14, #0x2F"); // slash starts a later basename component + emitter.instruction("b.ne __rt_tempnam_prefix_advance"); // retain the current basename candidate for ordinary bytes + emitter.instruction("add x12, x12, #1"); // skip the slash so the following bytes become the basename candidate + emitter.instruction("sub x13, x13, #1"); // remove the separator from the candidate length + emitter.instruction("str x12, [sp, #16]"); // persist the latest basename starting pointer + emitter.instruction("str x13, [sp, #24]"); // persist the bytes following that slash + emitter.instruction("b __rt_tempnam_prefix_scan"); // continue so the final slash wins + emitter.label("__rt_tempnam_prefix_advance"); + emitter.instruction("add x12, x12, #1"); // advance past an ordinary prefix byte + emitter.instruction("sub x13, x13, #1"); // consume that byte from the scan length + emitter.instruction("b __rt_tempnam_prefix_scan"); // inspect the following byte + emitter.label("__rt_tempnam_prefix_limit"); + emitter.instruction("ldr x14, [sp, #24]"); // reload the selected basename byte length + emitter.instruction("cmp x14, #63"); // PHP keeps at most 63 bytes of the basename prefix + emitter.instruction("b.ls __rt_tempnam_prefix_ready"); // no truncation is needed for short prefixes + emitter.instruction("mov x14, #63"); // cap the basename prefix to PHP's documented limit + emitter.instruction("str x14, [sp, #24]"); // use the capped length for template construction + emitter.label("__rt_tempnam_prefix_ready"); + emitter.instruction("str xzr, [sp, #40]"); // no system-temp fallback has been attempted yet + // -- build template path: dir + "/" + prefix + "XXXXXX" in _cstr_buf -- + emitter.label("__rt_tempnam_build"); abi::emit_symbol_address(emitter, "x9", "_cstr_buf"); // load page address of cstr buffer emitter.instruction("mov x10, x9"); // save buffer start @@ -79,7 +110,9 @@ pub fn emit_tempnam(emitter: &mut Emitter) { // -- call mkstemp to create the temp file (modifies XXXXXX in-place) -- emitter.instruction("str x10, [sp, #32]"); // save buffer start (clobbered by mkstemp) emitter.instruction("mov x0, x10"); // pass template buffer to mkstemp - emitter.bl_c("mkstemp"); // mkstemp(template), x0=fd + emitter.bl_c("mkstemp"); // mkstemp(template), x0=fd + emitter.instruction("cmp w0, #0"); // mkstemp returns a C int: a 64-bit compare reads -1 as 0xffffffff and misses every failure + emitter.instruction("b.lt __rt_tempnam_fail"); // return a null string sentinel instead of persisting the failed template // -- close the temp file (we only need the name) -- emitter.syscall(6); @@ -101,6 +134,29 @@ pub fn emit_tempnam(emitter: &mut Emitter) { emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address emitter.instruction("add sp, sp, #64"); // deallocate stack frame emitter.instruction("ret"); // return to caller + + // php retries in the system temporary directory when the requested one cannot + // hold the file, and says so with a notice. Without the retry tempnam() + // returned a path inside the unusable directory for a file it never created. + emitter.label("__rt_tempnam_fail"); + emitter.instruction("ldr x9, [sp, #40]"); // has the system-temp fallback already been attempted? + emitter.instruction("cbnz x9, __rt_tempnam_final_fail"); // a failed retry is the terminal failure + emitter.instruction("mov x9, #1"); // permit exactly one fallback attempt + emitter.instruction("str x9, [sp, #40]"); // record that the fallback is now in flight + emitter.instruction("bl __rt_php_temp_dir"); // x1 = directory pointer, x2 = directory length + emitter.instruction("cbz x2, __rt_tempnam_final_fail"); // an empty temporary directory is unusable + emitter.instruction("stp x1, x2, [sp, #0]"); // retry against the system temporary directory + abi::emit_symbol_address(emitter, "x1", "_tempnam_fallback_notice"); // php's notice explaining the fallback directory + emitter.instruction(&format!("mov x2, #{}", TEMPNAM_FALLBACK_NOTICE.len())); // exact diagnostic byte length + emitter.instruction("bl __rt_diag_warning"); // emit the notice unless @ suppression is active + emitter.instruction("b __rt_tempnam_build"); // rebuild the template in the system directory + + emitter.label("__rt_tempnam_final_fail"); + emitter.instruction("mov x1, #0"); // use a null pointer to represent PHP false to the EIR caller + emitter.instruction("mov x2, #0"); // clear the unused string length on failure + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // deallocate the failed tempnam frame + emitter.instruction("ret"); // return the failure sentinel } /// x86_64-specific implementation of `__rt_tempnam`. @@ -109,7 +165,9 @@ pub fn emit_tempnam(emitter: &mut Emitter) { /// buffer, calls mkstemp to rewrite the Xs in-place, closes the file descriptor, and returns /// the path as a heap-allocated persisted string via rax/rdx. /// -/// On mkstemp failure, frees the allocated buffer and returns an empty string (rax=0, rdx=0). +/// On Windows, a failed explicit directory retries once in the system temp +/// directory after a suppressible PHP notice. Other targets return the null +/// pointer sentinel that the EIR caller boxes as PHP `false`. fn emit_tempnam_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: tempnam ---"); @@ -117,16 +175,44 @@ fn emit_tempnam_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("push rbp"); // preserve the caller frame pointer while tempnam() uses path-component and template spill slots emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the preserved lengths, C strings, template pointer, and file descriptor - emitter.instruction("sub rsp, 64"); // reserve aligned spill slots for the directory/prefix lengths, C strings, template pointer, and mkstemp() fd + emitter.instruction("sub rsp, 80"); // reserve aligned spill slots, retry state, and owned fallback directory storage emitter.instruction("mov QWORD PTR [rbp - 8], rdx"); // preserve the elephc directory string length across C-string conversion and template construction emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // preserve the elephc prefix string length across C-string conversion and template construction emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // preserve the elephc prefix string pointer across the directory C-string conversion helper call + emitter.instruction("mov r8, rdi"); // start with the complete prefix as the basename candidate + emitter.instruction("mov rcx, rsi"); // start with the complete prefix length as the candidate length + emitter.instruction("xor r9d, r9d"); // prefix scan offset = 0 + emitter.label("__rt_tempnam_prefix_scan_x86"); + emitter.instruction("cmp r9, rsi"); // reached the end of the original prefix? + emitter.instruction("jae __rt_tempnam_prefix_limit_x86"); // select and cap the final basename candidate + emitter.instruction("movzx r10d, BYTE PTR [rdi + r9]"); // load the next prefix byte without requiring a C string + emitter.instruction("cmp r10b, 0x2F"); // slash begins a later basename component + emitter.instruction("je __rt_tempnam_prefix_separator_x86"); // keep only the bytes after the latest slash + if emitter.target.platform == Platform::Windows { + emitter.instruction("cmp r10b, 0x5C"); // backslash is also a Windows basename separator + emitter.instruction("je __rt_tempnam_prefix_separator_x86"); // keep only the bytes after the latest backslash + } + emitter.instruction("inc r9"); // advance to the following ordinary prefix byte + emitter.instruction("jmp __rt_tempnam_prefix_scan_x86"); // continue finding the final path separator + emitter.label("__rt_tempnam_prefix_separator_x86"); + emitter.instruction("lea r8, [rdi + r9 + 1]"); // make the byte after this separator the new basename candidate + emitter.instruction("mov rcx, rsi"); // recompute the remaining candidate length from the original prefix length + emitter.instruction("sub rcx, r9"); // exclude bytes before and including this separator + emitter.instruction("dec rcx"); // remove the separator itself from the candidate length + emitter.instruction("inc r9"); // resume scanning after this separator + emitter.instruction("jmp __rt_tempnam_prefix_scan_x86"); // a later separator overrides this candidate + emitter.label("__rt_tempnam_prefix_limit_x86"); + emitter.instruction("cmp rcx, 63"); // PHP truncates the basename prefix at 63 bytes + emitter.instruction("jbe __rt_tempnam_prefix_ready_x86"); // retain short basenames unchanged + emitter.instruction("mov rcx, 63"); // cap the template prefix to PHP's documented limit + emitter.label("__rt_tempnam_prefix_ready_x86"); + emitter.instruction("mov QWORD PTR [rbp - 24], r8"); // retain the selected basename pointer for template copying + emitter.instruction("mov QWORD PTR [rbp - 16], rcx"); // retain the selected and capped basename byte length emitter.instruction("call __rt_cstr"); // convert the elephc directory string in rax/rdx into a null-terminated C path in rax emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // preserve the C directory path pointer across the prefix conversion and template-construction loop - emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // reload the elephc prefix string pointer into the primary x86_64 string argument register - emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload the elephc prefix string length into the primary x86_64 string length register - emitter.instruction("call __rt_cstr2"); // convert the elephc prefix string into the secondary null-terminated C string buffer - emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // preserve the C prefix string pointer across template construction and mkstemp() + emitter.instruction("mov QWORD PTR [rbp - 64], 0"); // no system-temp fallback has been attempted yet + emitter.instruction("mov QWORD PTR [rbp - 72], 0"); // no owned fallback directory is awaiting release + emitter.label("__rt_tempnam_build_x86"); emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the directory string length before sizing the mutable mkstemp() template buffer emitter.instruction("add rax, QWORD PTR [rbp - 16]"); // include the prefix string length in the mutable mkstemp() template buffer size emitter.instruction("add rax, 8"); // include '/', the six X template bytes, and the trailing null terminator in the mutable buffer size @@ -149,7 +235,7 @@ fn emit_tempnam_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_tempnam_dir_done_x86"); emitter.instruction("mov BYTE PTR [r8], 0x2F"); // append the '/' separator between the directory component and the prefix component emitter.instruction("add r8, 1"); // advance the mutable template cursor past the inserted directory separator - emitter.instruction("mov r9, QWORD PTR [rbp - 40]"); // reload the C prefix string pointer before copying its bytes into the mutable template + emitter.instruction("mov r9, QWORD PTR [rbp - 24]"); // reload the basename prefix pointer before copying its bytes into the mutable template emitter.instruction("mov rcx, QWORD PTR [rbp - 16]"); // reload the prefix string length before copying the prefix bytes into the template emitter.label("__rt_tempnam_prefix_copy_x86"); emitter.instruction("test rcx, rcx"); // stop copying once every prefix byte has been materialized into the mutable template buffer @@ -168,10 +254,16 @@ fn emit_tempnam_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov BYTE PTR [r8 + 4], 0x58"); // append template X #5 into the mutable mkstemp() buffer emitter.instruction("mov BYTE PTR [r8 + 5], 0x58"); // append template X #6 into the mutable mkstemp() buffer emitter.instruction("mov BYTE PTR [r8 + 6], 0"); // append the trailing null terminator required by libc mkstemp() + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // load any owned system-temp directory after its bytes were copied + emitter.instruction("test rax, rax"); // did the fallback path allocate directory storage? + emitter.instruction("jz __rt_tempnam_template_ready_x86"); // caller-supplied directories use borrowed C-string storage + emitter.instruction("call __rt_heap_free"); // release the copied fallback directory before creating the file + emitter.instruction("mov QWORD PTR [rbp - 72], 0"); // prevent duplicate cleanup if the retry also fails + emitter.label("__rt_tempnam_template_ready_x86"); emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // pass the mutable template buffer to libc mkstemp(), which rewrites the trailing XXXXXX in place - emitter.instruction("call mkstemp"); // create a unique temp file and rewrite the mutable template buffer into the final temp path + emitter.emit_call_c("mkstemp"); // create the temp file (Windows: exclusive CreateFileW-backed shim) emitter.instruction("cmp eax, 0"); // detect a negative C int fd before trying to close it - emitter.instruction("jl __rt_tempnam_fail_x86"); // release the allocated template buffer and return the empty string when mkstemp() fails + emitter.instruction("jl __rt_tempnam_fail_x86"); // release the allocated template buffer and return false when mkstemp() fails emitter.instruction("cdqe"); // normalize the successful C int fd into the runtime's 64-bit descriptor value emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // preserve the returned file descriptor so the temp file can be closed before returning the path emitter.instruction("mov rdi, QWORD PTR [rbp - 56]"); // reload the mkstemp() file descriptor before closing the newly created temp file @@ -180,16 +272,71 @@ fn emit_tempnam_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rdx, QWORD PTR [rbp - 8]"); // rebuild the temp path length from the original directory string length emitter.instruction("add rdx, QWORD PTR [rbp - 16]"); // include the original prefix string length in the returned temp path length emitter.instruction("add rdx, 7"); // include the inserted '/' plus the six rewritten mkstemp() suffix characters in the returned temp path length - emitter.instruction("add rsp, 64"); // release the temporary tempnam() spill slots before returning + emitter.instruction("add rsp, 80"); // release the temporary tempnam() spill slots before returning emitter.instruction("pop rbp"); // restore the caller frame pointer before returning the owned temp path string emitter.instruction("ret"); // return the owned temp path string in the canonical x86_64 string result registers emitter.label("__rt_tempnam_fail_x86"); emitter.instruction("mov rax, QWORD PTR [rbp - 48]"); // reload the allocated template buffer pointer so the failed mkstemp() path can release it safely emitter.instruction("call __rt_heap_free"); // release the allocated template buffer when libc mkstemp() fails to create a temp file - emitter.instruction("xor eax, eax"); // return an empty string pointer when mkstemp() fails - emitter.instruction("xor edx, edx"); // return an empty string length when mkstemp() fails - emitter.instruction("add rsp, 64"); // release the temporary tempnam() spill slots on the failure path - emitter.instruction("pop rbp"); // restore the caller frame pointer before returning the empty string - emitter.instruction("ret"); // return the empty string result for the failed tempnam() helper + // php retries in the system temporary directory when the requested one cannot + // hold the file, and says so with a notice. Without the retry tempnam() + // returned a path inside the unusable directory for a file it never created. + let windows = emitter.target.platform == Platform::Windows; + let empty_target = if windows { + "__rt_tempnam_release_empty_fallback_x86" + } else { + "__rt_tempnam_final_fail_x86" + }; + emitter.instruction("cmp QWORD PTR [rbp - 64], 0"); // has the system-temp fallback already been attempted? + emitter.instruction("jne __rt_tempnam_final_fail_x86"); // a failed retry is the terminal failure + if windows { + emitter.instruction("call __rt_sys_get_temp_dir"); // retrieve PHP's Windows system temporary directory + } else { + emitter.instruction("call __rt_php_temp_dir"); // resolve TMPDIR, then P_tmpdir, as php does + } + emitter.instruction("test rax, rax"); // did the temp-directory query yield a path? + emitter.instruction("jz __rt_tempnam_final_fail_x86"); // a failed lookup leaves tempnam false + emitter.instruction("test rdx, rdx"); // is the returned directory non-empty? + emitter.instruction(&format!("jz {empty_target}")); // an empty temporary directory is unusable + emitter.instruction("mov QWORD PTR [rbp - 8], rdx"); // use the system-temp path length for the retry and return value + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // the directory bytes are copied by length, not by terminator + if windows { + emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // retain ownership until the directory bytes are copied + } else { + emitter.instruction("mov QWORD PTR [rbp - 72], 0"); // __rt_php_temp_dir borrows: there is nothing to release + } + emitter.instruction("mov QWORD PTR [rbp - 64], 1"); // permit exactly one fallback attempt + emitter.instruction("lea rdi, [rip + _tempnam_fallback_notice]"); // PHP notice explaining the fallback directory + emitter.instruction(&format!("mov esi, {}", TEMPNAM_FALLBACK_NOTICE.len())); // exact diagnostic byte length + emitter.instruction("call __rt_diag_warning"); // emit the notice unless @ suppression is active + emitter.instruction("jmp __rt_tempnam_build_x86"); // rebuild the template in the system directory + if windows { + emitter.label("__rt_tempnam_release_empty_fallback_x86"); + emitter.instruction("call __rt_heap_free"); // release the unusable empty system-temp string + } + emitter.label("__rt_tempnam_final_fail_x86"); + emitter.instruction("xor eax, eax"); // return a null pointer sentinel when mkstemp() fails + emitter.instruction("xor edx, edx"); // clear the unused string length on failure + emitter.instruction("add rsp, 80"); // release the temporary tempnam() spill slots on the failure path + emitter.instruction("pop rbp"); // restore the caller frame pointer before returning false + emitter.instruction("ret"); // return the failure sentinel for PHP false boxing +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::codegen::platform::Target; + + /// Verifies Windows retries a failed explicit directory through the system temp helper. + #[test] + fn windows_emits_system_directory_fallback() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_tempnam(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("call __rt_sys_get_temp_dir")); + assert!(asm.contains("lea rdi, [rip + _tempnam_fallback_notice]")); + assert!(asm.contains("call __rt_diag_warning")); + assert!(asm.contains("jmp __rt_tempnam_build_x86")); + } } diff --git a/src/codegen_support/runtime/io/tls_scheme.rs b/src/codegen_support/runtime/io/tls_scheme.rs new file mode 100644 index 0000000000..63afeb2346 --- /dev/null +++ b/src/codegen_support/runtime/io/tls_scheme.rs @@ -0,0 +1,215 @@ +//! Purpose: +//! Emits the `__rt_addr_tls_crypto_method` runtime helper, which reports whether a +//! `stream_socket_client()` address selects one of PHP's crypto transports. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::io`. +//! +//! Key details: +//! - php-src registers `ssl`, `sslv3`, `tls` and `tlsv1.0`..`tlsv1.3` as crypto +//! transports and sets `enable_on_connect` for each of them +//! (`php_openssl_ssl_socket_factory`, ext/openssl/xp_ssl.c), so the handshake +//! runs inside `stream_socket_client()`. `sslv2` is rejected by PHP outright and +//! is deliberately not matched here. +//! - Scheme names are case-insensitive. Every byte of interest (letters, `1`, `.`, +//! `:`, `/`) is unchanged or correctly folded by `| 0x20`, so one OR normalises +//! the whole prefix and the comparison becomes a handful of 64-bit tests. + +use crate::codegen_support::{emit::Emitter, platform::Arch}; + +/// Little-endian encoding of `"ssl://"` in the low 48 bits. +const SSL_PREFIX: u64 = 0x00_00_2f_2f_3a_6c_73_73; +/// Little-endian encoding of `"tls://"` in the low 48 bits. +const TLS_PREFIX: u64 = 0x00_00_2f_2f_3a_73_6c_74; +/// Little-endian encoding of the eight bytes `"sslv3://"`. +const SSLV3_PREFIX: u64 = 0x2f_2f_3a_33_76_6c_73_73; +/// Little-endian encoding of the eight bytes `"tlsv1.N:"`, with `N` cleared. +const TLSV1_PREFIX_BASE: u64 = 0x3a_00_2e_31_76_73_6c_74; + +/// Emits the `__rt_addr_tls_crypto_method` runtime helper. +/// +/// # ABI (AArch64) +/// - Input: `x0` = address pointer, `x1` = address byte length +/// - Output: `x0` = 1 when the address selects a TLS transport, otherwise 0 +/// +/// # ABI (x86_64) +/// - Input: `rdi` = address pointer, `rsi` = address byte length +/// - Output: `rax` = 1 when the address selects a TLS transport, otherwise 0 +pub fn emit_addr_tls_crypto_method(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_addr_tls_crypto_method_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.raw(" .p2align 2"); // keep the entry aligned after preceding runtime literals + emitter.comment("--- runtime: addr_is_tls_scheme ---"); + emitter.label_global("__rt_addr_tls_crypto_method"); + + emitter.instruction("cmp x1, #6"); // shortest TLS address prefix is "ssl://" + emitter.instruction("b.lo __rt_addr_tls_method_none"); // too short to name a crypto transport + emitter.instruction("ldrb w9, [x0]"); // assemble the first six bytes one at a time: + emitter.instruction("ldrb w10, [x0, #1]"); // the address may be shorter than eight bytes, so a + emitter.instruction("orr x9, x9, x10, lsl #8"); // single 64-bit load could read past the string + emitter.instruction("ldrb w10, [x0, #2]"); // (the runtime hands out exact-length slices) + emitter.instruction("orr x9, x9, x10, lsl #16"); // accumulate byte 2 + emitter.instruction("ldrb w10, [x0, #3]"); // load byte 3 + emitter.instruction("orr x9, x9, x10, lsl #24"); // accumulate byte 3 + emitter.instruction("ldrb w10, [x0, #4]"); // load byte 4 + emitter.instruction("orr x9, x9, x10, lsl #32"); // accumulate byte 4 + emitter.instruction("ldrb w10, [x0, #5]"); // load byte 5 + emitter.instruction("orr x9, x9, x10, lsl #40"); // accumulate byte 5 + emitter.instruction("mov x11, #0x2020"); // build the ASCII case-folding mask + emitter.instruction("movk x11, #0x2020, lsl #16"); // scheme names are case-insensitive in PHP + emitter.instruction("movk x11, #0x2020, lsl #32"); // '.', ':' and '/' are unchanged by this OR + emitter.instruction("orr x9, x9, x11"); // normalise the six-byte prefix to lower case + emitter.instruction(&format!("mov x12, #{}", SSL_PREFIX & 0xFFFF)); // compare against "ssl://" + emitter.instruction(&format!("movk x12, #{}, lsl #16", (SSL_PREFIX >> 16) & 0xFFFF)); // ... + emitter.instruction(&format!("movk x12, #{}, lsl #32", (SSL_PREFIX >> 32) & 0xFFFF)); // ... + emitter.instruction("cmp x9, x12"); // is the address an ssl:// address? + emitter.instruction("b.eq __rt_addr_tls_method_any"); // → TLS transport + emitter.instruction(&format!("mov x12, #{}", TLS_PREFIX & 0xFFFF)); // compare against "tls://" + emitter.instruction(&format!("movk x12, #{}, lsl #16", (TLS_PREFIX >> 16) & 0xFFFF)); // ... + emitter.instruction(&format!("movk x12, #{}, lsl #32", (TLS_PREFIX >> 32) & 0xFFFF)); // ... + emitter.instruction("cmp x9, x12"); // is the address a tls:// address? + emitter.instruction("b.eq __rt_addr_tls_method_any"); // → TLS transport + + // -- the remaining transports need eight normalised bytes -- + emitter.instruction("cmp x1, #8"); // "sslv3://" is exactly eight bytes + emitter.instruction("b.lo __rt_addr_tls_method_none"); // nothing longer can match + emitter.instruction("ldrb w10, [x0, #6]"); // load byte 6 + emitter.instruction("orr x9, x9, x10, lsl #48"); // accumulate byte 6 + emitter.instruction("ldrb w10, [x0, #7]"); // load byte 7 + emitter.instruction("orr x9, x9, x10, lsl #56"); // accumulate byte 7 + emitter.instruction("mov x11, #0x2020"); // rebuild the fold mask for the full word + emitter.instruction("movk x11, #0x2020, lsl #16"); // ... + emitter.instruction("movk x11, #0x2020, lsl #32"); // ... + emitter.instruction("movk x11, #0x2020, lsl #48"); // ... + emitter.instruction("orr x9, x9, x11"); // normalise the eight-byte prefix + crate::codegen_support::abi::emit_load_int_immediate(emitter, "x12", SSLV3_PREFIX as i64); // "sslv3://" + emitter.instruction("cmp x9, x12"); // is the address an sslv3:// address? + emitter.instruction("b.eq __rt_addr_tls_method_sslv3"); // → SSLv3-pinned transport + + // -- tlsv1.0:// .. tlsv1.3:// : the version digit is masked out before the compare -- + emitter.instruction("cmp x1, #10"); // "tlsv1.N://" is ten bytes + emitter.instruction("b.lo __rt_addr_tls_method_none"); // too short for a versioned transport + emitter.instruction("ldrb w10, [x0, #8]"); // byte 8 must be '/' + emitter.instruction("cmp w10, #0x2F"); // separator check + emitter.instruction("b.ne __rt_addr_tls_method_none"); // not a scheme separator + emitter.instruction("ldrb w10, [x0, #9]"); // byte 9 must be '/' + emitter.instruction("cmp w10, #0x2F"); // separator check + emitter.instruction("b.ne __rt_addr_tls_method_none"); // not a scheme separator + emitter.instruction("ubfx x10, x9, #48, #8"); // extract the version digit at byte 6 + emitter.instruction("sub x10, x10, #0x30"); // fold '0'..'3' to 0..3 + emitter.instruction("cmp x10, #3"); // reject any other digit + emitter.instruction("b.hi __rt_addr_tls_method_none"); // php only registers tlsv1.0..tlsv1.3 + emitter.instruction("and x9, x9, #0xFF00FFFFFFFFFFFF"); // clear the version digit before comparing + crate::codegen_support::abi::emit_load_int_immediate(emitter, "x12", TLSV1_PREFIX_BASE as i64); // "tlsv1.\0:" + emitter.instruction("cmp x9, x12"); // is the address a tlsv1.N:// address? + emitter.instruction("b.ne __rt_addr_tls_method_none"); // → plain transport + emitter.instruction("mov x0, #8"); // STREAM_CRYPTO_METHOD_TLSv1_N_CLIENT is 1 | (8 << N) + emitter.instruction("lsl x0, x0, x10"); // shift by the version digit recovered above + emitter.instruction("orr x0, x0, #1"); // set the client bit + emitter.instruction("ret"); // return the version-pinned crypto method + + emitter.label("__rt_addr_tls_method_any"); + emitter.instruction("mov x0, #121"); // STREAM_CRYPTO_METHOD_TLS_ANY_CLIENT (9|17|33|65) + emitter.instruction("ret"); // ssl:// and tls:// both default to it + + emitter.label("__rt_addr_tls_method_sslv3"); + emitter.instruction("mov x0, #5"); // STREAM_CRYPTO_METHOD_SSLv3_CLIENT + emitter.instruction("ret"); // the bridge rejects it, matching php without SSLv3 + + emitter.label("__rt_addr_tls_method_none"); + emitter.instruction("mov x0, #0"); // plain transport, no handshake on connect + emitter.instruction("ret"); // return "not a crypto transport" +} + +/// Emits the x86_64 implementation of `__rt_addr_tls_crypto_method`. +/// +/// Same algorithm as the AArch64 path; see `emit_addr_tls_crypto_method`. +fn emit_addr_tls_crypto_method_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: addr_is_tls_scheme ---"); + emitter.label_global("__rt_addr_tls_crypto_method"); + + emitter.instruction("cmp rsi, 6"); // shortest TLS address prefix is "ssl://" + emitter.instruction("jb __rt_addr_tls_method_none_x86"); // too short to name a crypto transport + emitter.instruction("xor r9, r9"); // accumulate the prefix one byte at a time so a + emitter.instruction("movzx r10d, BYTE PTR [rdi]"); // short address is never read past its end + emitter.instruction("or r9, r10"); // accumulate byte 0 + emitter.instruction("movzx r10d, BYTE PTR [rdi + 1]"); // load byte 1 + emitter.instruction("shl r10, 8"); // position byte 1 + emitter.instruction("or r9, r10"); // accumulate byte 1 + emitter.instruction("movzx r10d, BYTE PTR [rdi + 2]"); // load byte 2 + emitter.instruction("shl r10, 16"); // position byte 2 + emitter.instruction("or r9, r10"); // accumulate byte 2 + emitter.instruction("movzx r10d, BYTE PTR [rdi + 3]"); // load byte 3 + emitter.instruction("shl r10, 24"); // position byte 3 + emitter.instruction("or r9, r10"); // accumulate byte 3 + emitter.instruction("movzx r10d, BYTE PTR [rdi + 4]"); // load byte 4 + emitter.instruction("shl r10, 32"); // position byte 4 + emitter.instruction("or r9, r10"); // accumulate byte 4 + emitter.instruction("movzx r10d, BYTE PTR [rdi + 5]"); // load byte 5 + emitter.instruction("shl r10, 40"); // position byte 5 + emitter.instruction("or r9, r10"); // accumulate byte 5 + emitter.instruction("mov r11, 0x202020202020"); // ASCII case-folding mask for six bytes + emitter.instruction("or r9, r11"); // normalise the prefix to lower case + emitter.instruction(&format!("mov r11, {}", SSL_PREFIX)); // compare against "ssl://" + emitter.instruction("cmp r9, r11"); // is the address an ssl:// address? + emitter.instruction("je __rt_addr_tls_method_any_x86"); // → TLS transport + emitter.instruction(&format!("mov r11, {}", TLS_PREFIX)); // compare against "tls://" + emitter.instruction("cmp r9, r11"); // is the address a tls:// address? + emitter.instruction("je __rt_addr_tls_method_any_x86"); // → TLS transport + + // -- the remaining transports need eight normalised bytes -- + emitter.instruction("cmp rsi, 8"); // "sslv3://" is exactly eight bytes + emitter.instruction("jb __rt_addr_tls_method_none_x86"); // nothing longer can match + emitter.instruction("movzx r10d, BYTE PTR [rdi + 6]"); // load byte 6 + emitter.instruction("shl r10, 48"); // position byte 6 + emitter.instruction("or r9, r10"); // accumulate byte 6 + emitter.instruction("movzx r10d, BYTE PTR [rdi + 7]"); // load byte 7 + emitter.instruction("shl r10, 56"); // position byte 7 + emitter.instruction("or r9, r10"); // accumulate byte 7 + emitter.instruction("mov r11, 0x2020202020202020"); // fold mask for the full eight-byte word + emitter.instruction("or r9, r11"); // normalise the eight-byte prefix + emitter.instruction(&format!("mov r11, {}", SSLV3_PREFIX)); // "sslv3://" + emitter.instruction("cmp r9, r11"); // is the address an sslv3:// address? + emitter.instruction("je __rt_addr_tls_method_sslv3_x86"); // → SSLv3-pinned transport + + // -- tlsv1.0:// .. tlsv1.3:// : the version digit is masked out before the compare -- + emitter.instruction("cmp rsi, 10"); // "tlsv1.N://" is ten bytes + emitter.instruction("jb __rt_addr_tls_method_none_x86"); // too short for a versioned transport + emitter.instruction("cmp BYTE PTR [rdi + 8], 0x2F"); // byte 8 must be '/' + emitter.instruction("jne __rt_addr_tls_method_none_x86"); // not a scheme separator + emitter.instruction("cmp BYTE PTR [rdi + 9], 0x2F"); // byte 9 must be '/' + emitter.instruction("jne __rt_addr_tls_method_none_x86"); // not a scheme separator + emitter.instruction("mov r10, r9"); // copy the word to extract the version digit + emitter.instruction("shr r10, 48"); // move byte 6 into the low bits + emitter.instruction("movzx r10d, r10b"); // isolate the digit + emitter.instruction("sub r10, 0x30"); // fold '0'..'3' to 0..3 + emitter.instruction("cmp r10, 3"); // reject any other digit + emitter.instruction("ja __rt_addr_tls_method_none_x86"); // php only registers tlsv1.0..tlsv1.3 + emitter.instruction("mov r11, 0xFF00FFFFFFFFFFFF"); // mask clearing the version digit + emitter.instruction("and r9, r11"); // clear the digit before comparing + emitter.instruction(&format!("mov r11, {}", TLSV1_PREFIX_BASE)); // "tlsv1.\0:" + emitter.instruction("cmp r9, r11"); // is the address a tlsv1.N:// address? + emitter.instruction("jne __rt_addr_tls_method_none_x86"); // → plain transport + emitter.instruction("mov ecx, r10d"); // the shift count must live in cl + emitter.instruction("mov eax, 8"); // STREAM_CRYPTO_METHOD_TLSv1_N_CLIENT is 1 | (8 << N) + emitter.instruction("shl eax, cl"); // shift by the version digit recovered above + emitter.instruction("or eax, 1"); // set the client bit + emitter.instruction("ret"); // return the version-pinned crypto method + + emitter.label("__rt_addr_tls_method_any_x86"); + emitter.instruction("mov eax, 121"); // STREAM_CRYPTO_METHOD_TLS_ANY_CLIENT (9|17|33|65) + emitter.instruction("ret"); // ssl:// and tls:// both default to it + + emitter.label("__rt_addr_tls_method_sslv3_x86"); + emitter.instruction("mov eax, 5"); // STREAM_CRYPTO_METHOD_SSLv3_CLIENT + emitter.instruction("ret"); // the bridge rejects it, matching php without SSLv3 + + emitter.label("__rt_addr_tls_method_none_x86"); + emitter.instruction("xor eax, eax"); // plain transport, no handshake on connect + emitter.instruction("ret"); // return "not a crypto transport" +} diff --git a/src/codegen_support/runtime/io/tls_session_table.rs b/src/codegen_support/runtime/io/tls_session_table.rs new file mode 100644 index 0000000000..d442362dac --- /dev/null +++ b/src/codegen_support/runtime/io/tls_session_table.rs @@ -0,0 +1,222 @@ +//! Purpose: +//! Emits the bounded raw-descriptor-to-TLS-session map used by stream I/O. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` through the I/O runtime module. +//! +//! Key details: +//! - Raw Winsock `SOCKET` values are 64-bit handles and must never index a fixed array directly. +//! - The table holds 256 `(fd, session)` pairs; session zero marks an empty slot. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; +use crate::codegen_support::abi; + +/// Emits lookup, insertion, and removal helpers for the bounded TLS session table. +pub(crate) fn emit_tls_session_table(emitter: &mut Emitter) { + match emitter.target.arch { + Arch::AArch64 => emit_aarch64(emitter), + Arch::X86_64 => emit_x86_64(emitter), + } +} + +/// Emits the AArch64 TLS session-table helpers. +fn emit_aarch64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: bounded TLS session table ---"); + emitter.label_global("__rt_tls_session_get"); + abi::emit_symbol_address(emitter, "x9", "_tls_session_fds"); + abi::emit_symbol_address(emitter, "x10", "_tls_sessions"); + emitter.instruction("mov x11, #0"); // begin at the first bounded table slot + emitter.label("__rt_tls_get_loop"); + emitter.instruction("cmp x11, #256"); // have all TLS session slots been searched? + emitter.instruction("b.ge __rt_tls_get_miss"); // a full scan without a matching descriptor is a miss + emitter.instruction("ldr x12, [x10, x11, lsl #3]"); // load the candidate session handle + emitter.instruction("cbz x12, __rt_tls_get_next"); // empty slots cannot match a live descriptor + emitter.instruction("ldr x13, [x9, x11, lsl #3]"); // load the raw descriptor paired with this session + emitter.instruction("cmp x13, x0"); // does this slot belong to the requested descriptor? + emitter.instruction("b.eq __rt_tls_get_found"); // return the matching live TLS session + emitter.label("__rt_tls_get_next"); + emitter.instruction("add x11, x11, #1"); // advance to the next bounded table slot + emitter.instruction("b __rt_tls_get_loop"); // continue the linear lookup + emitter.label("__rt_tls_get_found"); + emitter.instruction("mov x0, x12"); // return the matched TLS session handle + emitter.instruction("ret"); // return to the stream I/O caller + emitter.label("__rt_tls_get_miss"); + emitter.instruction("mov x0, #0"); // zero means the descriptor is plain, not TLS-backed + emitter.instruction("ret"); // return the lookup miss + + emitter.label_global("__rt_tls_session_set"); + abi::emit_symbol_address(emitter, "x9", "_tls_session_fds"); + abi::emit_symbol_address(emitter, "x10", "_tls_sessions"); + emitter.instruction("mov x11, #0"); // begin by searching for an existing descriptor slot + emitter.label("__rt_tls_set_match_loop"); + emitter.instruction("cmp x11, #256"); // has the existing-slot scan completed? + emitter.instruction("b.ge __rt_tls_set_empty_start"); // no match requires an empty-slot scan + emitter.instruction("ldr x12, [x10, x11, lsl #3]"); // load the candidate session handle + emitter.instruction("cbz x12, __rt_tls_set_match_next"); // ignore unused slots during the match scan + emitter.instruction("ldr x13, [x9, x11, lsl #3]"); // load the descriptor paired with the live session + emitter.instruction("cmp x13, x0"); // is this the descriptor being installed? + emitter.instruction("b.eq __rt_tls_set_store"); // replace the existing session in place + emitter.label("__rt_tls_set_match_next"); + emitter.instruction("add x11, x11, #1"); // advance through live slots + emitter.instruction("b __rt_tls_set_match_loop"); // continue searching for a descriptor match + emitter.label("__rt_tls_set_empty_start"); + emitter.instruction("mov x11, #0"); // restart at slot zero to find free capacity + emitter.label("__rt_tls_set_empty_loop"); + emitter.instruction("cmp x11, #256"); // has every bounded slot proved occupied? + emitter.instruction("b.ge __rt_tls_set_full"); // fail cleanly instead of indexing beyond the table + emitter.instruction("ldr x12, [x10, x11, lsl #3]"); // inspect this slot's session marker + emitter.instruction("cbz x12, __rt_tls_set_store"); // session zero identifies an available slot + emitter.instruction("add x11, x11, #1"); // advance to the next candidate free slot + emitter.instruction("b __rt_tls_set_empty_loop"); // continue the bounded free-slot scan + emitter.label("__rt_tls_set_store"); + emitter.instruction("str x0, [x9, x11, lsl #3]"); // store the full-width raw descriptor key + emitter.instruction("str x1, [x10, x11, lsl #3]"); // store its TLS session handle + emitter.instruction("mov x0, #1"); // report successful insertion + emitter.instruction("ret"); // return to the attach path + emitter.label("__rt_tls_set_full"); + emitter.instruction("stp x29, x30, [sp, #-16]!"); // preserve the caller link while closing the untracked session + emitter.instruction("mov x0, x1"); // pass the untracked TLS session to elephc_tls_close + abi::emit_symbol_address(emitter, "x9", "_elephc_tls_close_fn"); + emitter.instruction("ldr x9, [x9]"); // load the published TLS close entry pointer + emitter.instruction("cbz x9, __rt_tls_set_full_closed"); // tolerate a missing bridge during defensive cleanup + emitter.emit_published_bridge_call("x9"); // close the unrepresentable session through the published TLS entry + emitter.label("__rt_tls_set_full_closed"); + emitter.instruction("ldp x29, x30, [sp], #16"); // restore the caller frame and return address + emitter.instruction("mov x0, #0"); // report bounded table exhaustion + emitter.instruction("ret"); // let the caller close the untracked TLS session + + emitter.label_global("__rt_tls_session_clear"); + abi::emit_symbol_address(emitter, "x9", "_tls_session_fds"); + abi::emit_symbol_address(emitter, "x10", "_tls_sessions"); + emitter.instruction("mov x11, #0"); // begin at the first bounded table slot + emitter.label("__rt_tls_clear_loop"); + emitter.instruction("cmp x11, #256"); // have all slots been searched? + emitter.instruction("b.ge __rt_tls_clear_miss"); // no matching descriptor means no session to clear + emitter.instruction("ldr x12, [x10, x11, lsl #3]"); // load the candidate session handle + emitter.instruction("cbz x12, __rt_tls_clear_next"); // skip unused slots + emitter.instruction("ldr x13, [x9, x11, lsl #3]"); // load the candidate raw descriptor + emitter.instruction("cmp x13, x0"); // does this slot match the requested descriptor? + emitter.instruction("b.eq __rt_tls_clear_found"); // clear and return the matching session + emitter.label("__rt_tls_clear_next"); + emitter.instruction("add x11, x11, #1"); // advance to the next bounded slot + emitter.instruction("b __rt_tls_clear_loop"); // continue the lookup + emitter.label("__rt_tls_clear_found"); + emitter.instruction("str xzr, [x10, x11, lsl #3]"); // mark the session slot empty first + emitter.instruction("str xzr, [x9, x11, lsl #3]"); // clear the stale descriptor key + emitter.instruction("mov x0, x12"); // return the removed session for close_notify + emitter.instruction("ret"); // return the removed session + emitter.label("__rt_tls_clear_miss"); + emitter.instruction("mov x0, #0"); // no live session was attached to this descriptor + emitter.instruction("ret"); // return the clear miss +} + +/// Emits the x86_64 TLS session-table helpers. +fn emit_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: bounded TLS session table ---"); + emitter.label_global("__rt_tls_session_get"); + abi::emit_symbol_address(emitter, "r8", "_tls_session_fds"); + abi::emit_symbol_address(emitter, "r9", "_tls_sessions"); + emitter.instruction("xor r10d, r10d"); // begin at the first bounded table slot + emitter.label("__rt_tls_get_loop_x86_64"); + emitter.instruction("cmp r10, 256"); // have all TLS session slots been searched? + emitter.instruction("jge __rt_tls_get_miss_x86_64"); // a full scan without a matching descriptor is a miss + emitter.instruction("mov r11, QWORD PTR [r9 + r10 * 8]"); // load the candidate session handle + emitter.instruction("test r11, r11"); // is this slot live? + emitter.instruction("jz __rt_tls_get_next_x86_64"); // empty slots cannot match a live descriptor + emitter.instruction("cmp QWORD PTR [r8 + r10 * 8], rdi"); // does this slot belong to the requested descriptor? + emitter.instruction("je __rt_tls_get_found_x86_64"); // return the matching live TLS session + emitter.label("__rt_tls_get_next_x86_64"); + emitter.instruction("add r10, 1"); // advance to the next bounded table slot + emitter.instruction("jmp __rt_tls_get_loop_x86_64"); // continue the linear lookup + emitter.label("__rt_tls_get_found_x86_64"); + emitter.instruction("mov rax, r11"); // return the matched TLS session handle + emitter.instruction("ret"); // return to the stream I/O caller + emitter.label("__rt_tls_get_miss_x86_64"); + emitter.instruction("xor eax, eax"); // zero means the descriptor is plain, not TLS-backed + emitter.instruction("ret"); // return the lookup miss + + emitter.label_global("__rt_tls_session_set"); + abi::emit_symbol_address(emitter, "r8", "_tls_session_fds"); + abi::emit_symbol_address(emitter, "r9", "_tls_sessions"); + emitter.instruction("xor r10d, r10d"); // begin by searching for an existing descriptor slot + emitter.label("__rt_tls_set_match_loop_x86_64"); + emitter.instruction("cmp r10, 256"); // has the existing-slot scan completed? + emitter.instruction("jge __rt_tls_set_empty_start_x86_64"); // no match requires an empty-slot scan + emitter.instruction("cmp QWORD PTR [r9 + r10 * 8], 0"); // is this slot unused? + emitter.instruction("je __rt_tls_set_match_next_x86_64"); // ignore unused slots during the match scan + emitter.instruction("cmp QWORD PTR [r8 + r10 * 8], rdi"); // is this the descriptor being installed? + emitter.instruction("je __rt_tls_set_store_x86_64"); // replace the existing session in place + emitter.label("__rt_tls_set_match_next_x86_64"); + emitter.instruction("add r10, 1"); // advance through live slots + emitter.instruction("jmp __rt_tls_set_match_loop_x86_64"); // continue searching for a descriptor match + emitter.label("__rt_tls_set_empty_start_x86_64"); + emitter.instruction("xor r10d, r10d"); // restart at slot zero to find free capacity + emitter.label("__rt_tls_set_empty_loop_x86_64"); + emitter.instruction("cmp r10, 256"); // has every bounded slot proved occupied? + emitter.instruction("jge __rt_tls_set_full_x86_64"); // fail cleanly instead of indexing beyond the table + emitter.instruction("cmp QWORD PTR [r9 + r10 * 8], 0"); // inspect this slot's session marker + emitter.instruction("je __rt_tls_set_store_x86_64"); // session zero identifies an available slot + emitter.instruction("add r10, 1"); // advance to the next candidate free slot + emitter.instruction("jmp __rt_tls_set_empty_loop_x86_64"); // continue the bounded free-slot scan + emitter.label("__rt_tls_set_store_x86_64"); + emitter.instruction("mov QWORD PTR [r8 + r10 * 8], rdi"); // store the full-width raw descriptor key + emitter.instruction("mov QWORD PTR [r9 + r10 * 8], rsi"); // store its TLS session handle + emitter.instruction("mov eax, 1"); // report successful insertion + emitter.instruction("ret"); // return to the attach path + emitter.label("__rt_tls_set_full_x86_64"); + emitter.instruction("sub rsp, 8"); // align the stack before closing the untracked session + emitter.instruction("mov rdi, rsi"); // pass the untracked TLS session to elephc_tls_close + abi::emit_load_symbol_to_reg(emitter, "r9", "_elephc_tls_close_fn", 0); // load the published TLS close entry pointer + emitter.instruction("test r9, r9"); // is defensive cleanup available? + emitter.instruction("jz __rt_tls_set_full_closed_x86_64"); // tolerate a missing bridge during defensive cleanup + emitter.emit_published_bridge_call("r9"); // close through the published TLS adapter + emitter.label("__rt_tls_set_full_closed_x86_64"); + emitter.instruction("add rsp, 8"); // restore the internal ABI stack position + emitter.instruction("xor eax, eax"); // report bounded table exhaustion + emitter.instruction("ret"); // let the caller close the untracked TLS session + + emitter.label_global("__rt_tls_session_clear"); + abi::emit_symbol_address(emitter, "r8", "_tls_session_fds"); + abi::emit_symbol_address(emitter, "r9", "_tls_sessions"); + emitter.instruction("xor r10d, r10d"); // begin at the first bounded table slot + emitter.label("__rt_tls_clear_loop_x86_64"); + emitter.instruction("cmp r10, 256"); // have all slots been searched? + emitter.instruction("jge __rt_tls_clear_miss_x86_64"); // no matching descriptor means no session to clear + emitter.instruction("mov r11, QWORD PTR [r9 + r10 * 8]"); // load the candidate session handle + emitter.instruction("test r11, r11"); // is this slot live? + emitter.instruction("jz __rt_tls_clear_next_x86_64"); // skip unused slots + emitter.instruction("cmp QWORD PTR [r8 + r10 * 8], rdi"); // does this slot match the requested descriptor? + emitter.instruction("je __rt_tls_clear_found_x86_64"); // clear and return the matching session + emitter.label("__rt_tls_clear_next_x86_64"); + emitter.instruction("add r10, 1"); // advance to the next bounded slot + emitter.instruction("jmp __rt_tls_clear_loop_x86_64"); // continue the lookup + emitter.label("__rt_tls_clear_found_x86_64"); + emitter.instruction("mov QWORD PTR [r9 + r10 * 8], 0"); // mark the session slot empty first + emitter.instruction("mov QWORD PTR [r8 + r10 * 8], 0"); // clear the stale descriptor key + emitter.instruction("mov rax, r11"); // return the removed session for close_notify + emitter.instruction("ret"); // return the removed session + emitter.label("__rt_tls_clear_miss_x86_64"); + emitter.instruction("xor eax, eax"); // no live session was attached to this descriptor + emitter.instruction("ret"); // return the clear miss +} + +#[cfg(test)] +mod tests { + use crate::codegen_support::platform::{Platform, Target}; + + use super::*; + + /// Verifies x86_64 lookups compare full-width descriptor keys rather than using raw indexing. + #[test] + fn x86_64_table_uses_full_width_descriptor_keys() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_tls_session_table(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("cmp QWORD PTR [r8 + r10 * 8], rdi")); + assert!(!asm.contains("_tls_sessions + rdi * 8")); + assert!(asm.contains("cmp r10, 256")); + } +} diff --git a/src/codegen_support/runtime/io/user_filter.rs b/src/codegen_support/runtime/io/user_filter.rs index d13295fed8..0b380d053e 100644 --- a/src/codegen_support/runtime/io/user_filter.rs +++ b/src/codegen_support/runtime/io/user_filter.rs @@ -23,7 +23,7 @@ //! contract or PHP's four-argument bucket-brigade form. Optional lifecycle //! hooks are honored, and `$this->params` is seeded before `onCreate()`. -use crate::codegen_support::{abi, emit::Emitter, platform::Arch}; +use crate::codegen_support::{abi, emit::Emitter, platform::{Arch, Platform}}; const USER_FILTER_REGISTRATIONS_CAP: u32 = 128; @@ -354,7 +354,7 @@ fn emit_stream_filter_attach_user_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("test r11, r11"); // check whether the runtime value is zero emitter.instruction("jz __rt_sfau_oncreate_skip_x86"); // method absent → skip the hook emitter.instruction("mov rdi, rax"); // $this in SysV first arg - emitter.instruction("call r11"); // call onCreate($this) → rax = bool + emitter.emit_platform_callback_call("r11", 1); emitter.instruction("test rax, rax"); // check whether the runtime value is zero emitter.instruction("jnz __rt_sfau_oncreate_skip_x86"); // truthy result → proceed // onCreate returned false → release the obj and bail without @@ -367,6 +367,14 @@ fn emit_stream_filter_attach_user_linux_x86_64(emitter: &mut Emitter) { // -- record state per direction bit -- emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload fd + if emitter.target.platform == Platform::Windows { + emitter.instruction("call __rt_win_stream_slot"); // map opaque Windows descriptor to a bounded filter-state slot + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // retain compact slot through direction bookkeeping + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // restore the filter instance after the slot lookup clobbers its result register + } else { + emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // non-Windows keeps its established descriptor-indexed behavior + } + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // all fixed filter tables consume the compact/legacy slot emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload mode emitter.instruction("mov rdx, QWORD PTR [rbp - 32]"); // reload resolved id (u8 in the low byte) abi::emit_symbol_address(emitter, "r9", "_user_filter_instances"); // instances table base @@ -374,22 +382,22 @@ fn emit_stream_filter_attach_user_linux_x86_64(emitter: &mut Emitter) { // mode & 1 → read direction emitter.instruction("test rsi, 1"); // STREAM_FILTER_READ bit set? emitter.instruction("jz __rt_sfau_skip_read_x86"); // skip read-direction state otherwise - emitter.instruction("mov r10, rdi"); // fd - emitter.instruction("add r10, r10"); // fd*2 — read slot - emitter.instruction("mov QWORD PTR [r9 + r10 * 8], rax"); // _user_filter_instances[fd*2] = obj + emitter.instruction("mov r10, rdi"); // compact stream slot + emitter.instruction("add r10, r10"); // slot*2 — read slot + emitter.instruction("mov QWORD PTR [r9 + r10 * 8], rax"); // _user_filter_instances[slot*2] = obj abi::emit_symbol_address(emitter, "r11", "_stream_read_filters"); // load runtime data address - emitter.instruction("mov BYTE PTR [r11 + rdi], dl"); // _stream_read_filters[fd] = id (low 8 bits) + emitter.instruction("mov BYTE PTR [r11 + rdi], dl"); // _stream_read_filters[slot] = id (low 8 bits) emitter.label("__rt_sfau_skip_read_x86"); // mode & 2 → write direction emitter.instruction("test rsi, 2"); // STREAM_FILTER_WRITE bit set? emitter.instruction("jz __rt_sfau_skip_write_x86"); // skip write-direction state otherwise - emitter.instruction("mov r10, rdi"); // fd - emitter.instruction("add r10, r10"); // fd*2 + emitter.instruction("mov r10, rdi"); // compact stream slot + emitter.instruction("add r10, r10"); // slot*2 emitter.instruction("add r10, 1"); // fd*2 + 1 — write slot - emitter.instruction("mov QWORD PTR [r9 + r10 * 8], rax"); // _user_filter_instances[fd*2 + 1] = obj + emitter.instruction("mov QWORD PTR [r9 + r10 * 8], rax"); // _user_filter_instances[slot*2 + 1] = obj abi::emit_symbol_address(emitter, "r11", "_stream_write_filters"); // load runtime data address - emitter.instruction("mov BYTE PTR [r11 + rdi], dl"); // _stream_write_filters[fd] = id (low 8 bits) + emitter.instruction("mov BYTE PTR [r11 + rdi], dl"); // _stream_write_filters[slot] = id (low 8 bits) emitter.label("__rt_sfau_skip_write_x86"); emitter.instruction("mov eax, 1"); // success @@ -496,6 +504,18 @@ fn emit_apply_user_stream_filter_linux_x86_64(emitter: &mut Emitter) { // -- look up the cached instance: _user_filter_instances[fd*2 + dir] -- emitter.instruction("mov r9, rdi"); // fd + if emitter.target.platform == Platform::Windows { + emitter.instruction("sub rsp, 32"); // reserve aligned spills for direction and filter input + emitter.instruction("mov QWORD PTR [rsp], rcx"); // preserve read/write direction across the slot-registry call + emitter.instruction("mov QWORD PTR [rsp + 8], rsi"); // preserve the filter input pointer across the slot-registry call + emitter.instruction("mov QWORD PTR [rsp + 16], rdx"); // preserve the filter input length clobbered by the slot registry + emitter.instruction("call __rt_win_stream_slot"); // map opaque Windows descriptor to bounded user-filter slot + emitter.instruction("mov r9, rax"); // instance table uses the compact slot, never a raw SOCKET + emitter.instruction("mov rcx, QWORD PTR [rsp]"); // restore the user-filter direction for instance indexing + emitter.instruction("mov rsi, QWORD PTR [rsp + 8]"); // restore the PHP string pointer for the filter callback + emitter.instruction("mov rdx, QWORD PTR [rsp + 16]"); // restore the PHP string length for the filter callback + emitter.instruction("add rsp, 32"); // release the aligned slot-registry spill area + } emitter.instruction("add r9, r9"); // fd*2 emitter.instruction("add r9, rcx"); // fd*2 + dir abi::emit_symbol_address(emitter, "r10", "_user_filter_instances"); // load runtime data address @@ -541,7 +561,7 @@ fn emit_apply_user_stream_filter_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("pop r10"); // restore filter method ptr // -- call filter($this=rdi, string=rsi/rdx) → returns string in rax/rdx -- - emitter.instruction("call r10"); // invoke the user filter method + emitter.emit_platform_callback_call("r10", 3); emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the post-filter string in rax/rdx @@ -674,6 +694,10 @@ fn emit_user_filter_release_fd_linux_x86_64(emitter: &mut Emitter) { let skip = format!("__rt_ufrf_dir{}_skip_x86", dir); let no_method = format!("__rt_ufrf_dir{}_no_method_x86", dir); emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // fd + if emitter.target.platform == Platform::Windows { + emitter.instruction("call __rt_win_stream_slot"); // map opaque Windows descriptor to bounded user-filter slot + emitter.instruction("mov rdi, rax"); // instance table uses only the compact slot + } emitter.instruction("mov r10, rdi"); // move runtime value between registers emitter.instruction("add r10, r10"); // fd*2 if dir == 1 { @@ -690,10 +714,14 @@ fn emit_user_filter_release_fd_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("test r14, r14"); // check whether the runtime value is zero emitter.instruction(&format!("jz {}", no_method)); // branch when the checked value is zero or equal emitter.instruction("mov rdi, r11"); // $this - emitter.instruction("call r14"); // call external helper + emitter.emit_platform_callback_call("r14", 1); emitter.label(&no_method); // Clear the slot. emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // prepare SysV call argument + if emitter.target.platform == Platform::Windows { + emitter.instruction("call __rt_win_stream_slot"); // map opaque Windows descriptor to bounded user-filter slot + emitter.instruction("mov rdi, rax"); // instance table uses only the compact slot + } emitter.instruction("mov r10, rdi"); // move runtime value between registers emitter.instruction("add r10, r10"); // advance runtime pointer or counter if dir == 1 { @@ -709,3 +737,91 @@ fn emit_user_filter_release_fd_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore caller frame pointer emitter.instruction("ret"); // return to caller } + +#[cfg(test)] +mod tests { + //! Purpose: + //! Regression tests for target-aware user stream-filter runtime emission. + //! + //! Called from: + //! - `cargo test` through Rust's test harness. + //! + //! Key details: + //! - Windows callback adapters need MSx64 shadow space and compact stream + //! lookup must preserve the pending filter payload. + + use crate::codegen_support::emit::Emitter; + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::*; + + /// Verifies Windows user-filter lifecycle and transform callbacks cross from + /// the runtime's SysV register staging to generated PHP's MSx64 ABI. Each + /// call needs shadow space and must target the adapter's relocated `r11`, + /// rather than directly calling the original vtable register. + #[test] + fn windows_x86_user_filter_callbacks_use_platform_adapter() { + let target = Target::new(Platform::Windows, Arch::X86_64); + + let mut attach = Emitter::new(target); + emit_stream_filter_attach_user(&mut attach); + let attach_asm = attach.output(); + assert!(attach_asm.contains("sub rsp, 32")); + assert!(attach_asm.contains("mov rcx, rdi")); + assert!(attach_asm.contains("call r11")); + let slot_lookup = attach_asm + .find("call __rt_win_stream_slot") + .expect("Windows user-filter attachment must allocate a compact stream slot"); + let restore_instance = attach_asm[slot_lookup..] + .find("mov rax, QWORD PTR [rbp - 40]") + .map(|offset| slot_lookup + offset) + .expect("slot lookup must restore the user-filter instance before recording it"); + let record_instance = attach_asm[restore_instance..] + .find("_user_filter_instances") + .map(|offset| restore_instance + offset) + .expect("restored user-filter instance must be recorded in the per-stream table"); + assert!(slot_lookup < restore_instance && restore_instance < record_instance); + + let mut apply = Emitter::new(target); + emit_apply_user_stream_filter(&mut apply); + let apply_asm = apply.output(); + let save_dir = apply_asm + .find("mov QWORD PTR [rsp], rcx") + .expect("Windows slot lookup must preserve the filter direction"); + let save_ptr = apply_asm + .find("mov QWORD PTR [rsp + 8], rsi") + .expect("Windows slot lookup must preserve the filter input pointer"); + let save_len = apply_asm + .find("mov QWORD PTR [rsp + 16], rdx") + .expect("Windows slot lookup must preserve the filter input length"); + let slot_lookup = apply_asm + .find("call __rt_win_stream_slot") + .expect("Windows user filters must map opaque descriptors to slots"); + let restore_dir = apply_asm + .find("mov rcx, QWORD PTR [rsp]") + .expect("Windows slot lookup must restore the filter direction"); + let restore_ptr = apply_asm + .find("mov rsi, QWORD PTR [rsp + 8]") + .expect("Windows slot lookup must restore the filter input pointer"); + let restore_len = apply_asm + .find("mov rdx, QWORD PTR [rsp + 16]") + .expect("Windows slot lookup must restore the filter input length"); + assert!(save_dir < save_ptr && save_ptr < save_len && save_len < slot_lookup); + assert!( + slot_lookup < restore_dir && restore_dir < restore_ptr && restore_ptr < restore_len + ); + assert!(apply_asm.contains("sub rsp, 32")); + assert!(apply_asm.contains("mov r8, rdx")); + assert!(apply_asm.contains("mov rdx, rsi")); + assert!(apply_asm.contains("mov rcx, rdi")); + assert!(apply_asm.contains("call r11")); + assert!(!apply_asm.contains("call r10\n")); + + let mut release = Emitter::new(target); + emit_user_filter_release_fd(&mut release); + let release_asm = release.output(); + assert_eq!(release_asm.matches("call r11").count(), 2); + assert_eq!(release_asm.matches("sub rsp, 32").count(), 2); + assert!(!release_asm.contains("call r14\n")); + } +} diff --git a/src/codegen_support/runtime/io/user_filter_brigade.rs b/src/codegen_support/runtime/io/user_filter_brigade.rs index 480c9633c1..1bae0df9f5 100644 --- a/src/codegen_support/runtime/io/user_filter_brigade.rs +++ b/src/codegen_support/runtime/io/user_filter_brigade.rs @@ -384,7 +384,7 @@ fn emit_user_filter_brigade_invoke_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rcx, QWORD PTR [rbp - 56]"); // consumed mixed emitter.instruction("mov r8, QWORD PTR [rbp - 64]"); // closing mixed emitter.instruction("mov r11, QWORD PTR [rbp - 32]"); // method ptr - emitter.instruction("call r11"); // call selected function pointer + emitter.emit_platform_callback_call("r11", 5); // -- Walk out_brigade._buckets, concatenate data -- emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // prepare SysV call argument @@ -468,3 +468,50 @@ fn emit_user_filter_brigade_invoke_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore caller frame pointer emitter.instruction("ret"); // return to caller } + +#[cfg(test)] +mod tests { + use crate::codegen_support::emit::Emitter; + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::*; + + /// Verifies the windows-x86_64 `__rt_user_filter_brigade_invoke` call site + /// stages the 5th int arg (closing) on the stack above the MSx64 shadow + /// space before the indirect `call r11` into the user filter's + /// `filter($in, $out, &$consumed, $closing)` method (finding F1, wide + /// reverse-ABI variant): without this staging the generated method would + /// read $this/in/out/consumed/closing from the wrong registers and slots + /// on windows-x86_64. + #[test] + fn test_windows_x86_64_user_filter_brigade_invoke_stages_stack_arg_before_call() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_user_filter_brigade_invoke(&mut emitter); + let asm = emitter.output(); + + let this_idx = asm.find("mov rdi, QWORD PTR [rbp - 8]").expect("expected SysV $this staging"); + let stack_idx = asm.find("mov QWORD PTR [rsp + 32], r8").expect("expected 5th-arg stack store"); + let call_idx = asm.find("call r11").expect("expected indirect call r11"); + assert!(this_idx < call_idx, "$this load must precede the indirect call"); + assert!(stack_idx < call_idx, "5th-arg stack store must precede the indirect call"); + assert!(asm.contains("sub rsp, 48"), "expected the windows call-site shadow-space sub"); + assert!(asm.contains("add rsp, 48"), "expected the windows call-site shadow-space add"); + } + + /// Verifies linux-x86_64 emission for `__rt_user_filter_brigade_invoke` + /// stays byte-identical to before the reverse-ABI staging was introduced: + /// the MSx64 stack staging is windows-x86_64-only, so a linux-x86_64 build + /// must keep the plain 5-register SysV call sequence. + #[test] + fn test_linux_x86_64_user_filter_brigade_invoke_has_no_msx64_stack_staging() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_user_filter_brigade_invoke(&mut emitter); + let asm = emitter.output(); + + assert!(!asm.contains("mov rcx, QWORD PTR [rbp - 8]")); + assert!(!asm.contains("mov QWORD PTR [rsp + 32], rax")); + assert!(asm.contains("mov rdi, QWORD PTR [rbp - 8]")); + assert!(!asm.contains("sub rsp, 48")); + assert!(!asm.contains("add rsp, 48")); + } +} diff --git a/src/codegen_support/runtime/io/user_wrapper.rs b/src/codegen_support/runtime/io/user_wrapper.rs index b732b4b422..3bf1c8b583 100644 --- a/src/codegen_support/runtime/io/user_wrapper.rs +++ b/src/codegen_support/runtime/io/user_wrapper.rs @@ -98,7 +98,7 @@ fn emit_user_wrapper_fclose_linux_x86_64(emitter: &mut Emitter) { emit_x86_method_lookup(emitter, "__rt_uwfclose_clear_x86", VTABLE_SLOT_CLOSE); // resolve stream_close method pointer into r11 // -- call stream_close($this) -- - emitter.instruction("call r11"); // invoke stream_close on the wrapper object + emitter.emit_platform_callback_call("r11", 1); emitter.label("__rt_uwfclose_clear_x86"); // -- free the handle slot so the synthetic fd cannot be reused stale -- @@ -168,7 +168,7 @@ fn emit_user_wrapper_fread_linux_x86_64(emitter: &mut Emitter) { // -- call stream_read($this, $count) → returns string in rax/rdx -- emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload the requested byte count - emitter.instruction("call r11"); // invoke stream_read on the wrapper object + emitter.emit_platform_callback_call("r11", 2); emitter.instruction("add rsp, 16"); // release the helper frame emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the wrapper's string result to the caller @@ -237,7 +237,7 @@ fn emit_user_wrapper_fwrite_linux_x86_64(emitter: &mut Emitter) { // -- call stream_write($this, $data) → returns int in rax -- emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // reload data string pointer as the second arg emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload data string length as the third arg - emitter.instruction("call r11"); // invoke stream_write on the wrapper object + emitter.emit_platform_callback_call("r11", 3); emitter.instruction("add rsp, 16"); // release the helper frame emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the wrapper's int result to the caller @@ -293,7 +293,7 @@ fn emit_user_wrapper_feof_linux_x86_64(emitter: &mut Emitter) { emit_x86_handle_lookup(emitter, "__rt_uwfeof_eof_x86"); // resolve obj into rdi, fall through on missing handles emit_x86_method_lookup(emitter, "__rt_uwfeof_eof_x86", VTABLE_SLOT_EOF); // resolve stream_eof method pointer into r11 - emitter.instruction("call r11"); // invoke stream_eof on the wrapper object + emitter.emit_platform_callback_call("r11", 1); emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the wrapper's bool result to the caller @@ -347,7 +347,7 @@ fn emit_user_wrapper_ftell_linux_x86_64(emitter: &mut Emitter) { emit_x86_handle_lookup(emitter, "__rt_uwftell_fail_x86"); // resolve obj into rdi, fall through on missing handles emit_x86_method_lookup(emitter, "__rt_uwftell_fail_x86", VTABLE_SLOT_TELL); // resolve stream_tell method pointer into r11 - emitter.instruction("call r11"); // invoke stream_tell on the wrapper object + emitter.emit_platform_callback_call("r11", 1); emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the wrapper's int result to the caller @@ -401,7 +401,7 @@ fn emit_user_wrapper_fflush_linux_x86_64(emitter: &mut Emitter) { emit_x86_handle_lookup(emitter, "__rt_uwfflush_ok_x86"); // resolve obj into rdi, fall through on missing handles emit_x86_method_lookup(emitter, "__rt_uwfflush_ok_x86", VTABLE_SLOT_FLUSH); // resolve stream_flush method pointer into r11 - emitter.instruction("call r11"); // invoke stream_flush on the wrapper object + emitter.emit_platform_callback_call("r11", 1); emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the wrapper's bool result to the caller @@ -468,7 +468,7 @@ fn emit_user_wrapper_fseek_linux_x86_64(emitter: &mut Emitter) { // -- call stream_seek($this, $offset, $whence) → returns bool/int in rax -- emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // reload offset emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload whence selector - emitter.instruction("call r11"); // invoke stream_seek on the wrapper object + emitter.emit_platform_callback_call("r11", 3); emitter.instruction("test rax, rax"); // did stream_seek return false? emitter.instruction("jz __rt_uwfseek_fail_x86"); // stream_seek returned false → PHP -1 failure sentinel emitter.instruction("xor eax, eax"); // stream_seek succeeded → PHP fseek returns 0 @@ -532,7 +532,7 @@ fn emit_user_wrapper_flock_linux_x86_64(emitter: &mut Emitter) { emit_x86_method_lookup(emitter, "__rt_uwflock_false_x86", VTABLE_SLOT_LOCK); // resolve stream_lock method pointer into r11 // -- call stream_lock($this, $operation) → returns bool in rax -- - emitter.instruction("call r11"); // invoke stream_lock on the wrapper object + emitter.emit_platform_callback_call("r11", 2); emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the wrapper's bool result to the caller @@ -591,7 +591,7 @@ fn emit_user_wrapper_ftruncate_linux_x86_64(emitter: &mut Emitter) { emit_x86_method_lookup(emitter, "__rt_uwftrunc_false_x86", VTABLE_SLOT_TRUNCATE); // resolve stream_truncate method pointer into r11 // -- call stream_truncate($this, $new_size) → returns bool in rax -- - emitter.instruction("call r11"); // invoke stream_truncate on the wrapper object + emitter.emit_platform_callback_call("r11", 2); emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the wrapper's bool result to the caller @@ -770,7 +770,7 @@ fn emit_user_wrapper_fstat_linux_x86_64(emitter: &mut Emitter) { emit_x86_method_lookup(emitter, "__rt_uwfstat_false_x86", VTABLE_SLOT_STAT); // resolve stream_stat method pointer into r11 // -- call stream_stat($this) → rax = raw return, normalized to a Mixed -- - emitter.instruction("call r11"); // invoke stream_stat on the wrapper object + emitter.emit_platform_callback_call("r11", 1); emitter.instruction("call __rt_box_wrapper_stat_result"); // normalize the type-erased return into a boxed Mixed emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the boxed Mixed stat array @@ -841,3 +841,39 @@ fn emit_x86_method_lookup(emitter: &mut Emitter, missing_label: &str, vtable_slo emitter.instruction("test r11, r11"); // is the method missing? emitter.instruction(&format!("jz {}", missing_label)); // method absent: take the fallback } + +#[cfg(test)] +mod tests { + use crate::codegen_support::emit::Emitter; + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::*; + + /// Verifies the windows-x86_64 `__rt_user_wrapper_fwrite` call site emits the + /// reverse-ABI SysV->MSx64 remap immediately before the indirect `call r11` + /// into the wrapper's `stream_write($this, $data)` method (finding F1, + /// reverse-ABI): without it, the generated method would read $this/ptr/len + /// from the wrong registers on windows-x86_64. + #[test] + fn test_windows_x86_64_user_wrapper_fwrite_remaps_before_indirect_call() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_user_wrapper_fwrite(&mut emitter); + let asm = emitter.output(); + + let remap_idx = asm.find("mov rcx, rdi").expect("expected SysV->MSx64 remap"); + let call_idx = asm.find("call r11").expect("expected indirect call r11"); + assert!(remap_idx < call_idx, "remap must precede the indirect stream_write call"); + } + + /// Verifies linux-x86_64 emission stays byte-identical to before the + /// reverse-ABI remap was introduced: the remap is windows-x86_64-only, so a + /// linux-x86_64 build must never see a `mov rcx, rdi` instruction. + #[test] + fn test_linux_x86_64_user_wrapper_fwrite_has_no_reverse_abi_remap() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_user_wrapper_fwrite(&mut emitter); + let asm = emitter.output(); + + assert!(!asm.contains("mov rcx, rdi")); + } +} diff --git a/src/codegen_support/runtime/io/user_wrapper_cast.rs b/src/codegen_support/runtime/io/user_wrapper_cast.rs index 6393301e32..26dbc58492 100644 --- a/src/codegen_support/runtime/io/user_wrapper_cast.rs +++ b/src/codegen_support/runtime/io/user_wrapper_cast.rs @@ -146,7 +146,7 @@ fn emit_user_wrapper_stream_cast_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jz __rt_uwcast_neg1_x86"); // method absent: not selectable // -- call stream_cast($this, $cast_as) → resource fd (or boxed/false) in rax -- - emitter.instruction("call r11"); // invoke stream_cast on the wrapper object + emitter.emit_platform_callback_call("r11", 2); // -- normalize the return to a raw int fd (see the AArch64 path for the rationale) -- emitter.instruction("test rax, rax"); // null/false-ish return? diff --git a/src/codegen_support/runtime/io/user_wrapper_dir.rs b/src/codegen_support/runtime/io/user_wrapper_dir.rs index 35fa8da247..cc66b0ebbe 100644 --- a/src/codegen_support/runtime/io/user_wrapper_dir.rs +++ b/src/codegen_support/runtime/io/user_wrapper_dir.rs @@ -255,7 +255,7 @@ fn emit_user_wrapper_opendir_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // path ptr → string-arg pair emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // path len → string-arg pair emitter.instruction("xor rcx, rcx"); // options = 0 - emitter.instruction("call r11"); // invoke dir_opendir on the wrapper object + emitter.emit_platform_callback_call("r11", 4); emitter.instruction("test rax, rax"); // did dir_opendir return false? emitter.instruction("jz __rt_uwod_fail_x86"); // dir_opendir returned false → free obj, false @@ -368,7 +368,7 @@ fn emit_user_wrapper_dir_readdir_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jz __rt_uwrd_empty_x86"); // missing dir_readdir → end of directory // -- call dir_readdir($this) → returns string in rax/rdx -- - emitter.instruction("call r11"); // invoke dir_readdir on the wrapper object + emitter.emit_platform_callback_call("r11", 1); emitter.instruction("test rdx, rdx"); // empty name (len 0) is the end-of-directory sentinel emitter.instruction("jz __rt_uwrd_empty_x86"); // box end-of-directory as false emitter.instruction("pop rbp"); // restore the caller frame pointer @@ -457,7 +457,7 @@ fn emit_user_wrapper_dir_closedir_linux_x86_64(emitter: &mut Emitter) { emitter.instruction(&format!("mov r11, QWORD PTR [r11 + {}]", VTABLE_DIR_CLOSEDIR_OFFSET)); // load the dir_closedir method pointer (slot 21) emitter.instruction("test r11, r11"); // class did not implement dir_closedir? emitter.instruction("jz __rt_uwcd_clear_x86"); // missing dir_closedir → just clear - emitter.instruction("call r11"); // invoke dir_closedir on the wrapper object + emitter.emit_platform_callback_call("r11", 1); emitter.label("__rt_uwcd_clear_x86"); // -- free the handle slot so the synthetic fd cannot be reused stale -- @@ -544,7 +544,7 @@ fn emit_user_wrapper_dir_rewinddir_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jz __rt_uwrw_false_x86"); // missing dir_rewinddir → false // -- call dir_rewinddir($this) → bool in rax -- - emitter.instruction("call r11"); // invoke dir_rewinddir on the wrapper object + emitter.emit_platform_callback_call("r11", 1); emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the wrapper's bool result diff --git a/src/codegen_support/runtime/io/user_wrapper_path_op.rs b/src/codegen_support/runtime/io/user_wrapper_path_op.rs index e7d9395a96..b288ba81d1 100644 --- a/src/codegen_support/runtime/io/user_wrapper_path_op.rs +++ b/src/codegen_support/runtime/io/user_wrapper_path_op.rs @@ -248,7 +248,7 @@ fn emit_user_wrapper_path_op_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // path len → string-arg pair emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // extra arg 3 (mode/options) emitter.instruction("mov r8, QWORD PTR [rbp - 40]"); // extra arg 4 (options) - emitter.instruction("call r11"); // invoke the wrapper path method + emitter.emit_platform_callback_call("r11", 5); emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // stash the bool result across the instance release emitter.instruction("mov rax, QWORD PTR [rbp - 48]"); // reload the throwaway wrapper object emitter.instruction("call __rt_decref_any"); // free the throwaway wrapper instance @@ -488,7 +488,22 @@ fn emit_user_wrapper_rename_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // from len → first string-arg pair emitter.instruction("mov rcx, QWORD PTR [rbp - 24]"); // to ptr → second string-arg pair emitter.instruction("mov r8, QWORD PTR [rbp - 32]"); // to len → second string-arg pair - emitter.instruction("call r11"); // invoke the wrapper's rename method + if emitter.platform == crate::codegen_support::platform::Platform::Windows { + // MSx64 assigns arguments by PHP value, not by independently packed + // machine words. After $this and the first two-word string consume + // rcx/rdx/r8, the second two-word string cannot be split at r9: both + // words belong in the caller stack area. + emitter.instruction("sub rsp, 48"); // reserve shadow space plus the complete second string pair + emitter.instruction("mov QWORD PTR [rsp + 32], rcx"); // destination string pointer in first overflow slot + emitter.instruction("mov QWORD PTR [rsp + 40], r8"); // destination string length in second overflow slot + emitter.instruction("mov r8, rdx"); // source string length → MSx64 arg register 3 + emitter.instruction("mov rdx, rsi"); // source string pointer → MSx64 arg register 2 + emitter.instruction("mov rcx, rdi"); // wrapper object → MSx64 arg register 1 + emitter.instruction("call r11"); // invoke generated wrapper method with composite ABI placement + emitter.instruction("add rsp, 48"); // release shadow and overflow slots + } else { + emitter.instruction("call r11"); // invoke the wrapper method through the SysV PHP ABI + } emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // stash the bool result across the instance release emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the throwaway wrapper object emitter.instruction("call __rt_decref_any"); // free the throwaway wrapper instance @@ -506,3 +521,87 @@ fn emit_user_wrapper_rename_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the bool result } + +#[cfg(test)] +mod tests { + use crate::codegen_support::emit::Emitter; + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::*; + + /// Verifies the windows-x86_64 `__rt_user_wrapper_path_op` call site stages + /// the 5th int arg (a4) on the stack above the MSx64 shadow space before the + /// indirect `call r11` into the wrapper's path method (finding F1, wide + /// reverse-ABI variant): without this staging the generated method would + /// read $this/path/a3/a4 from the wrong registers and slots on + /// windows-x86_64. + #[test] + fn test_windows_x86_64_user_wrapper_path_op_stages_stack_arg_before_call() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_user_wrapper_path_op(&mut emitter); + let asm = emitter.output(); + + let this_idx = asm.find("mov rdi, QWORD PTR [rbp - 48]").expect("expected SysV $this staging"); + let stack_idx = asm.find("mov QWORD PTR [rsp + 32], r8").expect("expected 5th-arg stack store"); + let call_idx = asm.find("call r11").expect("expected indirect call r11"); + assert!(this_idx < call_idx, "$this load must precede the indirect call"); + assert!(stack_idx < call_idx, "5th-arg stack store must precede the indirect call"); + assert_eq!(asm.matches("sub rsp, 48").count(), 2, "expected the frame sub plus the windows call-site sub"); + assert_eq!(asm.matches("add rsp, 48").count(), 2, "expected the frame add plus the windows call-site add"); + } + + /// Verifies linux-x86_64 emission for `__rt_user_wrapper_path_op` stays + /// byte-identical to before the reverse-ABI staging was introduced: the + /// MSx64 stack staging is windows-x86_64-only, so a linux-x86_64 build must + /// keep the plain 5-register SysV call sequence. + #[test] + fn test_linux_x86_64_user_wrapper_path_op_has_no_msx64_stack_staging() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_user_wrapper_path_op(&mut emitter); + let asm = emitter.output(); + + assert!(!asm.contains("mov rcx, QWORD PTR [rbp - 48]")); + assert!(!asm.contains("mov QWORD PTR [rsp + 32], rax")); + assert!(asm.contains("mov rdi, QWORD PTR [rbp - 48]")); + assert_eq!(asm.matches("sub rsp, 48").count(), 1, "expected only the frame sub on linux-x86_64"); + assert_eq!(asm.matches("add rsp, 48").count(), 1, "expected only the frame add on linux-x86_64"); + } + + /// Verifies Windows stages the entire second string on the stack because a + /// two-word PHP string cannot be split between r9 and the MSx64 stack area. + #[test] + fn test_windows_x86_64_user_wrapper_rename_stages_stack_arg_before_call() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_user_wrapper_rename(&mut emitter); + let asm = emitter.output(); + + let this_idx = asm.find("mov rdi, QWORD PTR [rbp - 40]").expect("expected SysV $this staging"); + let to_ptr_idx = asm + .find("mov QWORD PTR [rsp + 32], rcx") + .expect("expected destination pointer stack store"); + let to_len_idx = asm + .find("mov QWORD PTR [rsp + 40], r8") + .expect("expected destination length stack store"); + let call_idx = asm.find("call r11").expect("expected indirect call r11"); + assert!(this_idx < call_idx, "$this load must precede the indirect call"); + assert!(to_ptr_idx < call_idx, "destination pointer store must precede the indirect call"); + assert!(to_len_idx < call_idx, "destination length store must precede the indirect call"); + assert_eq!(asm.matches("sub rsp, 48").count(), 2, "expected the frame sub plus the windows call-site sub"); + assert_eq!(asm.matches("add rsp, 48").count(), 2, "expected the frame add plus the windows call-site add"); + } + + /// Verifies linux-x86_64 emission for `__rt_user_wrapper_rename` stays + /// byte-identical to before the reverse-ABI staging was introduced. + #[test] + fn test_linux_x86_64_user_wrapper_rename_has_no_msx64_stack_staging() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_user_wrapper_rename(&mut emitter); + let asm = emitter.output(); + + assert!(!asm.contains("mov rcx, QWORD PTR [rbp - 40]")); + assert!(!asm.contains("mov QWORD PTR [rsp + 32], rax")); + assert!(asm.contains("mov rdi, QWORD PTR [rbp - 40]")); + assert_eq!(asm.matches("sub rsp, 48").count(), 1, "expected only the frame sub on linux-x86_64"); + assert_eq!(asm.matches("add rsp, 48").count(), 1, "expected only the frame add on linux-x86_64"); + } +} diff --git a/src/codegen_support/runtime/io/user_wrapper_set_option.rs b/src/codegen_support/runtime/io/user_wrapper_set_option.rs index 49bdcce84a..b703ec4ea4 100644 --- a/src/codegen_support/runtime/io/user_wrapper_set_option.rs +++ b/src/codegen_support/runtime/io/user_wrapper_set_option.rs @@ -102,7 +102,7 @@ fn emit_user_wrapper_set_option_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jz __rt_uwsetopt_false_x86"); // missing method → false // -- call stream_set_option($this, $option, $arg1, $arg2) → bool in rax -- - emitter.instruction("call r11"); // invoke stream_set_option on the wrapper object + emitter.emit_platform_callback_call("r11", 4); emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the wrapper's bool result diff --git a/src/codegen_support/runtime/io/user_wrapper_url_stat.rs b/src/codegen_support/runtime/io/user_wrapper_url_stat.rs index 2fc77fdd65..781e390da4 100644 --- a/src/codegen_support/runtime/io/user_wrapper_url_stat.rs +++ b/src/codegen_support/runtime/io/user_wrapper_url_stat.rs @@ -258,7 +258,7 @@ fn emit_user_wrapper_url_stat_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // path ptr → string-arg pair emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // path len → string-arg pair emitter.instruction("mov rcx, QWORD PTR [rbp - 24]"); // url_stat flags - emitter.instruction("call r11"); // invoke url_stat on the throwaway wrapper object + emitter.emit_platform_callback_call("r11", 4); emitter.instruction("call __rt_box_wrapper_stat_result"); // normalize the type-erased return into a boxed Mixed emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the boxed result across the wrapper-instance release emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the throwaway wrapper object diff --git a/src/codegen_support/runtime/io/var_dump_walk.rs b/src/codegen_support/runtime/io/var_dump_walk.rs index 96d430bcd6..55ca90261e 100644 --- a/src/codegen_support/runtime/io/var_dump_walk.rs +++ b/src/codegen_support/runtime/io/var_dump_walk.rs @@ -527,6 +527,11 @@ pub fn emit_var_dump_array_bool(emitter: &mut Emitter) { /// `__rt_var_dump_emit_float_line`: emit ` float(VAL)\n` for a single /// f64. Input: AArch64 d0 / x86_64 xmm0 = value. +/// +/// `var_dump` renders floats at PHP's `serialize_precision = -1` (shortest +/// round-trip) rather than the `precision = 14` setting `__rt_ftoa` +/// implements for `echo`/`(string)`, so this calls the Inf/NaN-safe +/// `__rt_var_dump_ftoa` wrapper instead of `__rt_ftoa`. pub fn emit_var_dump_emit_float_line(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_var_dump_emit_float_line_linux_x86_64(emitter); @@ -546,8 +551,8 @@ pub fn emit_var_dump_emit_float_line(emitter: &mut Emitter) { emitter.instruction("mov x2, #8"); // len(" float(") = 8 emitter.instruction("bl __rt_vd_write"); // write x1/x2 through the ob/web-aware stdout sink (register-preserving) - // ftoa(d0) → x1=ptr, x2=len - emitter.instruction("bl __rt_ftoa"); // call runtime helper + // var_dump_ftoa(d0) → x1=ptr, x2=len (serialize_precision=-1, Inf/NaN-safe) + emitter.instruction("bl __rt_var_dump_ftoa"); // call runtime helper emitter.instruction("bl __rt_vd_write"); // write x1/x2 through the ob/web-aware stdout sink (register-preserving) // Emit ")\n" @@ -575,8 +580,8 @@ fn emit_var_dump_emit_float_line_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov edx, 8"); // prepare SysV call argument emitter.instruction("call __rt_vd_write"); // write rsi/rdx through the ob/web-aware stdout sink (register-preserving) - emitter.instruction("movsd xmm0, QWORD PTR [rbp - 8]"); // reload xmm0 for ftoa - emitter.instruction("call __rt_ftoa"); // rax=ptr, rdx=len + emitter.instruction("movsd xmm0, QWORD PTR [rbp - 8]"); // reload xmm0 for var_dump_ftoa + emitter.instruction("call __rt_var_dump_ftoa"); // rax=ptr, rdx=len emitter.instruction("mov rsi, rax"); // prepare SysV call argument emitter.instruction("call __rt_vd_write"); // write rsi/rdx through the ob/web-aware stdout sink (register-preserving) diff --git a/src/codegen_support/runtime/mod.rs b/src/codegen_support/runtime/mod.rs index 26bc9761eb..8a7abdc589 100644 --- a/src/codegen_support/runtime/mod.rs +++ b/src/codegen_support/runtime/mod.rs @@ -28,6 +28,7 @@ mod pointers; pub(crate) mod spl; mod strings; mod system; +mod win32; /// zval pack/unpack bridge helpers (elephc values ↔ PHP zval structs). mod zval; @@ -49,3 +50,5 @@ pub(crate) use fibers::{ FIBER_STATE_SUSPENDED, FIBER_STATE_TERMINATED, FIBER_TRANSFER_VALUE_OFFSET, FIBER_USER_ARG_MAX_OFFSET, }; +/// Windows C-shim registry lookup, consulted by `Emitter::emit_call_c`. +pub(crate) use win32::windows_c_shim_name; diff --git a/src/codegen_support/runtime/objects/call_destructor.rs b/src/codegen_support/runtime/objects/call_destructor.rs index cd2049399a..feb8cceaef 100644 --- a/src/codegen_support/runtime/objects/call_destructor.rs +++ b/src/codegen_support/runtime/objects/call_destructor.rs @@ -96,6 +96,9 @@ fn emit_call_object_destructor_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: call_object_destructor ---"); emitter.label_global("__rt_call_object_destructor"); + emitter.instruction("push rbp"); // preserve the caller frame pointer for both callback paths + emitter.instruction("mov rbp, rsp"); // establish one unwindable frame for the whole helper + emitter.instruction("sub rsp, 16"); // reserve the eval callback object spill slot emitter.instruction("test rdi, rdi"); // null receiver → nothing to destruct emitter.instruction("jz __rt_call_object_destructor_ret"); // skip the lookup for a null object @@ -108,14 +111,9 @@ fn emit_call_object_destructor_x86_64(emitter: &mut Emitter) { emitter.instruction("jz __rt_call_object_destructor_static_x86"); // no eval callback installed → use static class table emitter.instruction("or eax, 0x80000000"); // mark destruction in progress before boxing borrowed $this emitter.instruction("mov DWORD PTR [rdi - 12], eax"); // persist the guard flag in the refcount field - emitter.instruction("push rbp"); // align the stack and save the caller frame pointer - emitter.instruction("mov rbp, rsp"); // establish the eval callback frame - emitter.instruction("sub rsp, 16"); // reserve a spill slot for the object pointer emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the object pointer across the callback - emitter.instruction("call r10"); // ask eval whether it owns and destructed this object + emitter.emit_native_bridge_call("r10", 1); emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // restore the object pointer after the callback - emitter.instruction("add rsp, 16"); // release the eval callback spill slot - emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("test rax, rax"); // did eval handle this dynamic object? emitter.instruction("jnz __rt_call_object_destructor_ret"); // eval handled the dynamic object → skip static lookup emitter.instruction("mov eax, DWORD PTR [rdi - 12]"); // reload the refcount after an eval miss @@ -132,11 +130,53 @@ fn emit_call_object_destructor_x86_64(emitter: &mut Emitter) { emitter.instruction("mov eax, DWORD PTR [rdi - 12]"); // eax = object refcount (header offset -12) emitter.instruction("or eax, 0x80000000"); // mark destruction in progress so a balanced self-ref cannot re-enter the free path emitter.instruction("mov DWORD PTR [rdi - 12], eax"); // persist the guard flag in the refcount field - emitter.instruction("push rbp"); // align the stack and save the caller frame pointer - emitter.instruction("mov rbp, rsp"); // establish the helper frame - emitter.instruction("call r10"); // invoke ::__destruct with rdi = $this (borrowed) - emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.emit_platform_callback_call("r10", 1); emitter.label("__rt_call_object_destructor_ret"); + emitter.instruction("leave"); // release the shared callback frame and restore rbp emitter.instruction("ret"); // return to __rt_object_free_deep to release the storage } + +#[cfg(test)] +mod tests { + use crate::codegen_support::emit::Emitter; + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::*; + + /// Verifies the windows-x86_64 `__rt_call_object_destructor` call site emits + /// the reverse-ABI SysV->MSx64 remap immediately before the indirect + /// `call r10` into the generated `__destruct` method (finding F1, + /// reverse-ABI): without it, the generated destructor would read `$this` + /// from the wrong register on windows-x86_64. + #[test] + fn test_windows_x86_64_call_object_destructor_remaps_before_indirect_call() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_call_object_destructor(&mut emitter); + let asm = emitter.output(); + + let remap_idx = asm.find("mov rcx, rdi").expect("expected SysV->MSx64 remap"); + let shadow_idx = asm + .find("sub rsp, 32") + .expect("expected MSx64 shadow space before the destructor call"); + let call_idx = asm.find("call r11").expect("expected relocated indirect call r11"); + assert!( + shadow_idx < remap_idx && remap_idx < call_idx, + "shadow reservation and remap must precede the indirect __destruct call" + ); + assert!(asm[call_idx..].contains("add rsp, 32")); + } + + /// Verifies linux-x86_64 emission stays byte-identical to before the + /// reverse-ABI remap was introduced: the remap is windows-x86_64-only, so a + /// linux-x86_64 build must never see a `mov rcx, rdi` instruction. + #[test] + fn test_linux_x86_64_call_object_destructor_has_no_reverse_abi_remap() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_call_object_destructor(&mut emitter); + let asm = emitter.output(); + + assert!(!asm.contains("mov rcx, rdi")); + assert!(!asm.contains("sub rsp, 32")); + } +} diff --git a/src/codegen_support/runtime/objects/new_by_name.rs b/src/codegen_support/runtime/objects/new_by_name.rs index 625d2e352e..a6cdbf1d20 100644 --- a/src/codegen_support/runtime/objects/new_by_name.rs +++ b/src/codegen_support/runtime/objects/new_by_name.rs @@ -219,7 +219,7 @@ fn emit_new_by_name_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jz __rt_nbn_no_propinit_x86"); // class has no property defaults: skip emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the object across the thunk (obj_size slot is free now) emitter.instruction("mov rdi, rax"); // this = object (first SysV argument) - emitter.instruction("call r10"); // _class_propinit_(this) + emitter.emit_platform_callback_call("r10", 1); emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // restore the object pointer (the thunk may clobber rax) emitter.label("__rt_nbn_no_propinit_x86"); emitter.label("__rt_nbn_return_allocated_x86"); diff --git a/src/codegen_support/runtime/objects/stdclass.rs b/src/codegen_support/runtime/objects/stdclass.rs index 58cc125c52..43657aae47 100644 --- a/src/codegen_support/runtime/objects/stdclass.rs +++ b/src/codegen_support/runtime/objects/stdclass.rs @@ -466,8 +466,8 @@ fn emit_stdclass_new_x86_64(emitter: &mut Emitter) { abi::emit_load_symbol_to_reg(emitter, "r10", "_stdclass_class_id", 0); // load the compile-time stdClass class_id emitter.instruction("mov QWORD PTR [rax], r10"); // store class_id at obj+0 - emitter.instruction("mov rax, 8"); // initial capacity = 8 slots (mixed_from_value first arg) - emitter.instruction("mov rdi, 7"); // value_type = 7 (boxed Mixed) + emitter.instruction("mov rdi, 8"); // initial capacity = 8 slots + emitter.instruction("mov rsi, 7"); // value_type = 7 (boxed Mixed) emitter.instruction("call __rt_hash_new"); // rax = empty hash pointer emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the saved obj pointer @@ -678,8 +678,8 @@ fn emit_stdclass_set_x86_64(emitter: &mut Emitter) { emitter.instruction("test r10, r10"); // already allocated? emitter.instruction("jne __rt_stdclass_set_have_hash"); // skip lazy init when present - emitter.instruction("mov rax, 8"); // initial capacity = 8 slots - emitter.instruction("mov rdi, 7"); // value_type = 7 (boxed Mixed) + emitter.instruction("mov rdi, 8"); // initial capacity = 8 slots + emitter.instruction("mov rsi, 7"); // value_type = 7 (boxed Mixed) emitter.instruction("call __rt_hash_new"); // rax = empty hash pointer emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload obj emitter.instruction("mov QWORD PTR [r10 + 8], rax"); // store the new hash_ptr at obj+8 @@ -702,3 +702,27 @@ fn emit_stdclass_set_x86_64(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore caller frame pointer emitter.instruction("ret"); // return (no return value) } + +#[cfg(test)] +mod tests { + use crate::codegen_support::{ + emit::Emitter, + platform::{Arch, Platform, Target}, + }; + + use super::emit_stdclass_new_x86_64; + + /// Verifies stdClass seeds hash capacity and value type in the internal SysV argument registers. + #[test] + fn x86_64_stdclass_new_stages_hash_new_arguments_in_runtime_registers() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_stdclass_new_x86_64(&mut emitter); + let asm = emitter.output(); + let call = asm + .find(" call __rt_hash_new\n") + .expect("stdClass constructor should allocate its dynamic-property hash"); + let setup = &asm[..call]; + + assert!(setup.ends_with(" mov rdi, 8\n mov rsi, 7\n")); + } +} diff --git a/src/codegen_support/runtime/spl/doubly_linked_list.rs b/src/codegen_support/runtime/spl/doubly_linked_list.rs index 223c607b88..bb456bc7de 100644 --- a/src/codegen_support/runtime/spl/doubly_linked_list.rs +++ b/src/codegen_support/runtime/spl/doubly_linked_list.rs @@ -2520,22 +2520,8 @@ fn emit_throw_exception_x86_64( 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, 56"); // request Throwable payload storage (message/code/previous) - emitter.instruction("call __rt_heap_alloc"); // allocate the exception object payload - emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(6))); // stamp the canonical x86_64 heap-kind word (magic + kind 6 throwable) - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object - abi::emit_load_symbol_to_reg(emitter, "r10", class_id_symbol, 0); // load the exception class id for this program - emitter.instruction("mov QWORD PTR [rax], r10"); // store class id at object header - abi::emit_symbol_address(emitter, "r10", message_symbol); // 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], {}", message_len)); // store static exception message length - emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero - emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null - 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 + abi::emit_load_symbol_to_reg(emitter, "rdi", class_id_symbol, 0); // pass the runtime exception class id + abi::emit_symbol_address(emitter, "rsi", message_symbol); // pass the static exception message pointer + emitter.instruction(&format!("mov rdx, {}", message_len)); // pass the static exception message length + emitter.instruction("jmp __rt_throw_static_exception"); // allocate and publish through an independent unwind frame } diff --git a/src/codegen_support/runtime/spl/fixed_array.rs b/src/codegen_support/runtime/spl/fixed_array.rs index a342612f15..fa63802af3 100644 --- a/src/codegen_support/runtime/spl/fixed_array.rs +++ b/src/codegen_support/runtime/spl/fixed_array.rs @@ -748,11 +748,11 @@ fn emit_new_x86_64(emitter: &mut Emitter) { emitter.label_global("__rt_spl_fixed_new"); emitter.instruction("push rbp"); // preserve caller frame pointer for constructor spills emitter.instruction("mov rbp, rsp"); // establish constructor frame - emitter.instruction("sub rsp, 24"); // reserve class id, size, and object spills + emitter.instruction("sub rsp, 32"); // reserve aligned class id, size, and object spills emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save concrete SplFixedArray class id emitter.instruction("cmp rsi, 0"); // reject negative sizes emitter.instruction("jge __rt_spl_fixed_new_size_ok"); // keep non-negative sizes - emitter.instruction("add rsp, 24"); // release constructor spills before throwing + emitter.instruction("add rsp, 32"); // release constructor spills before throwing emitter.instruction("pop rbp"); // restore caller frame pointer before throwing emit_throw_exception_x86_64( emitter, @@ -789,7 +789,7 @@ fn emit_new_x86_64(emitter: &mut Emitter) { emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload object pointer emitter.instruction(&format!("mov QWORD PTR [r11 + {}], rax", SPL_FIXED_STORAGE_OFFSET)); // object.storage = fixed-array storage emitter.instruction("mov rax, r11"); // return initialized SplFixedArray object - emitter.instruction("add rsp, 24"); // release constructor spills + emitter.instruction("add rsp, 32"); // release constructor spills emitter.instruction("pop rbp"); // restore caller frame pointer emitter.instruction("ret"); // return object pointer } @@ -1403,7 +1403,7 @@ fn emit_boxed_null_call_x86_64(emitter: &mut Emitter) { } /// Emits the aarch64 exception-throw sequence for SplFixedArray runtime errors. -/// Allocates a 32-byte Throwable payload, stamps it as heap kind 6 (object), stores the +/// Allocates a 56-byte Throwable payload, stamps it as heap kind 6 (object), stores the /// class id from `class_id_symbol`, the static message pointer, the message length, /// zero exception code, publishes the exception to `_exc_value`, and branches to `__rt_throw_current`. /// Clobbers: x0, x1, x2, x9. @@ -1432,7 +1432,7 @@ fn emit_throw_exception_aarch64( } /// Emits the x86_64 exception-throw sequence for SplFixedArray runtime errors. -/// Allocates a 32-byte Throwable payload, stamps it with the x86_64 heap-kind word (HEAP_MAGIC_LO +/// Allocates a 56-byte Throwable payload, stamps it with the x86_64 heap-kind word (HEAP_MAGIC_LO /// + kind 6), stores the class id via RIP-relative Lea, the static message pointer, /// the message length, zero exception code, publishes the exception to `_exc_value`, /// and jumps to `__rt_throw_current`. Preserves rbp, keeps stack 16-byte aligned. @@ -1442,22 +1442,8 @@ fn emit_throw_exception_x86_64( 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, 56"); // request Throwable payload storage (message/code/previous) - emitter.instruction("call __rt_heap_alloc"); // allocate the exception object payload - emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(6))); // stamp the canonical x86_64 heap-kind word (magic + kind 6 throwable) - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object - abi::emit_load_symbol_to_reg(emitter, "r10", class_id_symbol, 0); // load the exception class id for this program - emitter.instruction("mov QWORD PTR [rax], r10"); // store class id at object header - abi::emit_symbol_address(emitter, "r10", message_symbol); // 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], {}", message_len)); // store static exception message length - emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero - emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null - 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 + abi::emit_load_symbol_to_reg(emitter, "rdi", class_id_symbol, 0); // pass the runtime exception class id + abi::emit_symbol_address(emitter, "rsi", message_symbol); // pass the static exception message pointer + emitter.instruction(&format!("mov rdx, {}", message_len)); // pass the static exception message length + emitter.instruction("jmp __rt_throw_static_exception"); // allocate and publish through an independent unwind frame } diff --git a/src/codegen_support/runtime/strings/ftoa.rs b/src/codegen_support/runtime/strings/ftoa.rs index afbb65772c..770f1cb311 100644 --- a/src/codegen_support/runtime/strings/ftoa.rs +++ b/src/codegen_support/runtime/strings/ftoa.rs @@ -1,16 +1,33 @@ //! Purpose: -//! Emits the `__rt_ftoa` runtime helper assembly for float-to-string conversion. -//! Keeps PHP byte-string pointer/length behavior and target-specific ABI variants in one focused emitter. +//! Emits the `__rt_ftoa` runtime helper assembly for float-to-string conversion at +//! PHP's `precision` setting (default 14 significant digits) — the formatter behind +//! `echo`, `(string)`/`strval`, string interpolation, `.` concat, `print_r`, and +//! `settype($x,'string')`. Keeps PHP byte-string pointer/length behavior and +//! target-specific ABI variants in one focused emitter. //! //! Called from: //! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::strings`. //! //! Key details: //! - String helpers use PHP pointer/length pairs and target ABI return registers; heap-backed results must remain refcount-compatible. +//! - Reproduces php-src's `zend_gcvt(value, ndigit=14, dec_point='.', exponent='E', buf)` +//! layout without linking `zend_dtoa`: a fixed 14-significant-digit `snprintf("%.*e", 13, x)` +//! supplies the rounded mantissa, a trailing-zero trim on that fixed digit string emulates +//! `zend_dtoa`'s mode-2 digit suppression, and the exponential-vs-fixed threshold +//! (`decpt > 14 || decpt < -3`) and layout mirror `Zend/zend_strtod.c::zend_gcvt` exactly +//! (verified against the php-src source, not guessed). +//! - `var_dump`/`var_export`/`json_encode`/`serialize` use PHP's OTHER float precision +//! (`serialize_precision = -1`, shortest round-trip) and must NOT call this helper — they +//! route to `__rt_json_ftoa` (`crate::codegen_support::runtime::system::json_ftoa`) instead. +//! - Non-finite inputs bypass the `snprintf`/`zend_gcvt` machinery entirely: the exponent-field +//! bit test (`0x7FF`) and sign/mantissa follow-up mirror the proven `__rt_serialize` float +//! special-casing (`crate::codegen_support::runtime::system::serialize`), emitting PHP's +//! `"INF"`/`"-INF"`/`"NAN"` spellings directly into `_concat_buf`. -use crate::codegen_support::{emit::Emitter, platform::Arch}; +use crate::codegen_support::{abi, emit::Emitter, platform::Arch}; -/// Converts a double-precision float to a PHP-compatible byte string. +/// Converts a double-precision float to a PHP-compatible byte string at PHP's +/// `precision` setting (14 significant digits): `zend_gcvt(x, 14, '.', 'E', buf)`. /// /// # Input /// - ARM64: `d0` holds the float value @@ -21,13 +38,19 @@ use crate::codegen_support::{emit::Emitter, platform::Arch}; /// - x86_64: `rax` = pointer to string, `rdx` = length /// /// # Behavior -/// Formats the float using `snprintf` with `"%.14G"` format into the global -/// `_concat_buf` buffer at the current `_concat_off` cursor, then advances -/// `_concat_off` by the number of characters written. +/// Non-finite inputs (`INF`/`-INF`/`NAN`) are detected via the IEEE-754 exponent +/// field and written verbatim. Finite inputs are rounded to 14 significant digits +/// with `snprintf("%.*e", 13, x)`, trailing zeros are trimmed to emulate +/// `zend_dtoa`'s digit suppression, and the trimmed digit string is laid out per +/// `zend_gcvt`: fixed-decimal when `-3 <= decpt <= 14`, exponential otherwise +/// (`decpt` = the position of the decimal point relative to the first significant +/// digit). Both layouts write into the global `_concat_buf` at the current +/// `_concat_off` cursor and advance the cursor by the bytes written. /// /// # ABI Notes /// - Apple ARM64: variadic floats are passed on the stack, not in SIMD registers /// - Linux x86_64: delegates to `emit_ftoa_linux_x86_64`; uses SysV variadic ABI with `eax=1` to indicate one SIMD register argument +/// - windows-x86_64: `emit_ftoa_linux_x86_64` routes every libc call through `Emitter::emit_call_c` so `snprintf`/`strtod`/`strtol` reach the registered `__rt_sys_*` msvcrt shims instead of a raw SysV-staged import call pub fn emit_ftoa(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_ftoa_linux_x86_64(emitter); @@ -35,50 +58,213 @@ pub fn emit_ftoa(emitter: &mut Emitter) { } emitter.blank(); - emitter.comment("--- runtime: ftoa ---"); + emitter.comment("--- runtime: ftoa (precision=14 zend_gcvt) ---"); emitter.label_global("__rt_ftoa"); - // -- set up stack frame (64 bytes) -- - emitter.instruction("sub sp, sp, #64"); // allocate 64 bytes on the stack - emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address - emitter.instruction("add x29, sp, #48"); // establish new frame pointer + // -- set up stack frame (128 bytes): variadic slots, %.*e scratch, saved -- + // -- input double, and the callee-saved p_eff/neg/E registers -- + emitter.instruction("sub sp, sp, #128"); // allocate the ftoa scratch frame + emitter.instruction("stp x29, x30, [sp, #112]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #112"); // establish new frame pointer + emitter.instruction("stp x19, x20, [sp, #96]"); // save callee-saved registers x19 (p_eff) / x20 (neg) + emitter.instruction("str x21, [sp, #88]"); // save callee-saved register x21 (exponent E) + emitter.instruction("str d0, [sp, #80]"); // save the input double for re-formatting and compare - // -- get current concat_buf position -- - crate::codegen_support::abi::emit_symbol_address(emitter, "x9", "_concat_off"); - emitter.instruction("ldr x10, [x9]"); // load current write offset - emitter.instruction("str x10, [sp, #32]"); // save original offset on stack - emitter.instruction("str x9, [sp, #40]"); // save offset variable address on stack + // -- non-finite check: exponent field == 0x7FF means Inf or NaN -- + emitter.instruction("fmov x9, d0"); // grab the raw bit pattern of the input double + emitter.instruction("lsr x10, x9, #52"); // shift the exponent field into the low bits + emitter.instruction("and x10, x10, #0x7ff"); // isolate the 11-bit exponent + emitter.instruction("cmp x10, #0x7ff"); // is the exponent all ones (Inf/NaN)? + emitter.instruction("b.eq __rt_ftoa_nonfinite"); // dispatch to the INF/-INF/NAN literal writer - crate::codegen_support::abi::emit_symbol_address(emitter, "x11", "_concat_buf"); - emitter.instruction("add x0, x11, x10"); // compute output buffer: concat_buf + offset - emitter.instruction("str x0, [sp, #24]"); // save output buffer start on stack + // -- finite path: snprintf("%.*e", 13, x) -> 14 significant digits -- + emitter.instruction("mov x19, #13"); // fixed precision: 14 significant digits (1 + 13 fractional) + emitter.instruction("add x0, sp, #16"); // snprintf buffer = scratch area + emitter.instruction("mov x1, #64"); // scratch buffer size + abi::emit_symbol_address(emitter, "x2", "_fmt_star_e"); + emitter.instruction("str x19, [sp, #0]"); // Apple variadic arg 0: int precision (on stack) + emitter.instruction("str d0, [sp, #8]"); // Apple variadic arg 1: the double value (on stack) + emitter.instruction("ldr x3, [sp, #0]"); // AAPCS64 variadic arg 0: int precision (in x3) + emitter.bl_c("snprintf"); // format x at 14 significant digits (double stays in d0 for AAPCS64) - // -- call snprintf(buf, 32, "%.14G", double) -- - emitter.instruction("mov x1, #32"); // buffer size limit = 32 bytes - crate::codegen_support::abi::emit_symbol_address(emitter, "x2", "_fmt_g"); // load page address of format string "%.14G" - // -- Apple ARM64 variadic ABI: float arg goes on stack, not in SIMD reg -- - emitter.instruction("str d0, [sp]"); // push double onto stack for variadic call - emitter.bl_c("snprintf"); // call snprintf; returns char count in x0 + // -- parse the sign from scratch[0] -- + emitter.instruction("ldrb w9, [sp, #16]"); // scratch[0] + emitter.instruction("cmp w9, #45"); // is the first byte a '-' sign? + emitter.instruction("cset x20, eq"); // neg = 1 when the value is negative - // -- x0 = number of chars written -- - emitter.instruction("mov x2, x0"); // save string length as return value + // -- parse the decimal exponent E: the 'e' marker sits at a fixed offset -- + // -- (neg + 1 digit + '.' + 13 frac digits) since precision is always 13 -- + emitter.instruction("add x0, sp, #16"); // base of scratch string + emitter.instruction("add x0, x0, x20"); // + neg + emitter.instruction("add x0, x0, #16"); // address of the exponent text after 'e' (sign + digits) + emitter.instruction("mov x1, #0"); // strtol endptr = NULL + emitter.instruction("mov x2, #10"); // base 10 + emitter.bl_c("strtol"); // E = parsed decimal exponent + emitter.instruction("mov x21, x0"); // keep E in a callee-saved register - // -- update concat_off by chars written -- - emitter.instruction("ldr x9, [sp, #40]"); // reload offset variable address - emitter.instruction("ldr x10, [sp, #32]"); // reload original offset - emitter.instruction("add x10, x10, x2"); // new offset = original + chars written - emitter.instruction("str x10, [x9]"); // store updated offset + // -- trim trailing zero fractional digits to emulate zend_dtoa's mode-2 -- + // -- digit suppression; p_eff = number of significant fractional digits -- + emitter.instruction("add x14, sp, #16"); // base of scratch string + emitter.instruction("add x14, x14, x20"); // + neg + emitter.instruction("add x14, x14, #2"); // frac digit base = scratch + neg + 2 + emitter.instruction("mov x19, #13"); // p_eff starts at the full 13 fractional digits + emitter.instruction("mov x13, #12"); // j = index of the last fractional digit (0-based) + emitter.label("__rt_ftoa_trim"); + emitter.instruction("cmp x19, #0"); // nothing left to trim? + emitter.instruction("b.eq __rt_ftoa_trim_done"); // stop once every digit is trimmed + emitter.instruction("ldrb w15, [x14, x13]"); // load the fractional digit at index j + emitter.instruction("cmp w15, #48"); // is it '0'? + emitter.instruction("b.ne __rt_ftoa_trim_done"); // stop at the first non-zero trailing digit + emitter.instruction("sub x19, x19, #1"); // trim one more trailing zero + emitter.instruction("sub x13, x13, #1"); // move to the previous fractional digit + emitter.instruction("b __rt_ftoa_trim"); // continue trimming + emitter.label("__rt_ftoa_trim_done"); - // -- set return pointer -- - emitter.instruction("ldr x1, [sp, #24]"); // return pointer to start of formatted string + // -- choose decimal vs exponential layout by decimal-point position -- + // -- (zend_gcvt: exponential when decpt > ndigit(14) or decpt < -3) -- + emitter.instruction("add x9, x21, #1"); // decpt = E + 1 + emitter.instruction("cmn x9, #3"); // compare decpt against -3 + emitter.instruction("b.lt __rt_ftoa_exp"); // decpt < -3 -> exponential form + emitter.instruction("cmp x9, #14"); // compare decpt against 14 + emitter.instruction("b.gt __rt_ftoa_exp"); // decpt > 14 -> exponential form + + // -- decimal form: snprintf("%.*f", max(0, p_eff - E), x) into concat_buf -- + emitter.instruction("sub x9, x19, x21"); // fracdigits = p_eff - E + emitter.instruction("cmp x9, #0"); // is the fractional digit count negative? + emitter.instruction("csel x9, x9, xzr, ge"); // clamp negatives to zero (integer-valued) + emitter.instruction("str x9, [sp, #0]"); // Apple variadic arg 0: fractional digit count (stack) + abi::emit_symbol_address(emitter, "x10", "_concat_off"); + emitter.instruction("ldr x11, [x10]"); // current concat offset + abi::emit_symbol_address(emitter, "x12", "_concat_buf"); + emitter.instruction("add x0, x12, x11"); // destination = concat_buf + offset + emitter.instruction("mov x1, #48"); // destination size cap + abi::emit_symbol_address(emitter, "x2", "_fmt_star_f"); + emitter.instruction("ldr d0, [sp, #80]"); // reload the input double + emitter.instruction("str d0, [sp, #8]"); // Apple variadic arg 1: the double value (stack) + emitter.instruction("ldr x3, [sp, #0]"); // AAPCS64 variadic arg 0: fractional digit count (x3) + emitter.bl_c("snprintf"); // format the decimal digits (double stays in d0 for AAPCS64) + emitter.instruction("mov x2, x0"); // result length = bytes written + abi::emit_symbol_address(emitter, "x9", "_concat_off"); + emitter.instruction("ldr x10, [x9]"); // original offset (unchanged by snprintf) + abi::emit_symbol_address(emitter, "x11", "_concat_buf"); + emitter.instruction("add x1, x11, x10"); // result pointer = concat_buf + offset + emitter.instruction("add x10, x10, x2"); // advance the cursor past the digits + emitter.instruction("str x10, [x9]"); // publish the new concat offset + emitter.instruction("b __rt_ftoa_done"); // finished decimal layout + + // -- exponential form: d.dddd...E[+-]exp with the leftover p_eff digits -- + emitter.label("__rt_ftoa_exp"); + abi::emit_symbol_address(emitter, "x9", "_concat_off"); + emitter.instruction("ldr x10, [x9]"); // current concat offset + abi::emit_symbol_address(emitter, "x11", "_concat_buf"); + emitter.instruction("add x12, x11, x10"); // cursor = concat_buf + offset + emitter.instruction("mov x1, x12"); // remember the result start pointer + emitter.instruction("cbz x20, __rt_ftoa_exp_first"); // skip sign when the value is non-negative + emitter.instruction("mov w13, #45"); // ASCII '-' + emitter.instruction("strb w13, [x12], #1"); // emit the sign and advance the cursor + emitter.label("__rt_ftoa_exp_first"); + emitter.instruction("add x13, sp, #16"); // base of scratch string + emitter.instruction("ldrb w14, [x13, x20]"); // first significant digit (after optional sign) + emitter.instruction("strb w14, [x12], #1"); // emit the leading digit + emitter.instruction("mov w13, #46"); // ASCII '.' + emitter.instruction("strb w13, [x12], #1"); // emit the decimal point + emitter.instruction("cbnz x19, __rt_ftoa_exp_frac"); // p_eff>0 copies the fractional digits + emitter.instruction("mov w13, #48"); // ASCII '0' (mantissa needs one fraction digit) + emitter.instruction("strb w13, [x12], #1"); // emit "0" so the mantissa reads "d.0" + emitter.instruction("b __rt_ftoa_exp_e"); // continue with the exponent marker + emitter.label("__rt_ftoa_exp_frac"); + emitter.instruction("add x13, sp, #16"); // base of scratch string + emitter.instruction("add x13, x13, x20"); // skip optional sign + emitter.instruction("add x13, x13, #2"); // skip the leading digit and '.' + emitter.instruction("mov x14, #0"); // fractional copy index + emitter.label("__rt_ftoa_exp_frac_loop"); + emitter.instruction("cmp x14, x19"); // copied all p_eff fractional digits? + emitter.instruction("b.ge __rt_ftoa_exp_e"); // mantissa fraction complete + emitter.instruction("ldrb w15, [x13, x14]"); // load fractional digit + emitter.instruction("strb w15, [x12], #1"); // emit fractional digit + emitter.instruction("add x14, x14, #1"); // advance the fractional index + emitter.instruction("b __rt_ftoa_exp_frac_loop"); // copy the next fractional digit + emitter.label("__rt_ftoa_exp_e"); + emitter.instruction("mov w13, #69"); // ASCII 'E' (precision-14 layout is always uppercase) + emitter.instruction("strb w13, [x12], #1"); // emit the exponent marker + emitter.instruction("cmp x21, #0"); // is the exponent negative? + emitter.instruction("b.ge __rt_ftoa_exp_pos"); // positive exponent uses '+' + emitter.instruction("mov w13, #45"); // ASCII '-' + emitter.instruction("strb w13, [x12], #1"); // emit the exponent sign + emitter.instruction("neg x21, x21"); // make the exponent magnitude positive + emitter.instruction("b __rt_ftoa_exp_mag"); // emit the magnitude digits + emitter.label("__rt_ftoa_exp_pos"); + emitter.instruction("mov w13, #43"); // ASCII '+' + emitter.instruction("strb w13, [x12], #1"); // emit the exponent sign + emitter.label("__rt_ftoa_exp_mag"); + emitter.instruction("mov x13, #100"); // divisor for the hundreds digit + emitter.instruction("udiv x14, x21, x13"); // hundreds = E / 100 + emitter.instruction("msub x15, x14, x13, x21"); // remainder = E - hundreds*100 + emitter.instruction("mov x13, #10"); // divisor for the tens digit + emitter.instruction("udiv x16, x15, x13"); // tens = remainder / 10 + emitter.instruction("msub x17, x16, x13, x15"); // ones = remainder - tens*10 + emitter.instruction("cbz x14, __rt_ftoa_exp_tens"); // skip hundreds when it is zero + emitter.instruction("add w13, w14, #48"); // hundreds digit to ASCII + emitter.instruction("strb w13, [x12], #1"); // emit hundreds digit + emitter.instruction("add w13, w16, #48"); // tens digit to ASCII + emitter.instruction("strb w13, [x12], #1"); // emit tens digit + emitter.instruction("b __rt_ftoa_exp_ones"); // emit the ones digit + emitter.label("__rt_ftoa_exp_tens"); + emitter.instruction("cbz x16, __rt_ftoa_exp_ones"); // skip tens when it (and hundreds) are zero + emitter.instruction("add w13, w16, #48"); // tens digit to ASCII + emitter.instruction("strb w13, [x12], #1"); // emit tens digit + emitter.label("__rt_ftoa_exp_ones"); + emitter.instruction("add w13, w17, #48"); // ones digit to ASCII + emitter.instruction("strb w13, [x12], #1"); // emit ones digit + emitter.instruction("sub x2, x12, x1"); // result length = cursor - start + abi::emit_symbol_address(emitter, "x9", "_concat_off"); + emitter.instruction("ldr x10, [x9]"); // original concat offset + emitter.instruction("add x10, x10, x2"); // advance past the emitted bytes + emitter.instruction("str x10, [x9]"); // publish the new concat offset + emitter.instruction("b __rt_ftoa_done"); // finished exponential layout + + // -- non-finite: INF / -INF / NAN, matching __rt_serialize's classification -- + emitter.label("__rt_ftoa_nonfinite"); + emitter.instruction("lsl x10, x9, #12"); // drop the sign+exponent to test the mantissa + emitter.instruction("cbnz x10, __rt_ftoa_nan"); // non-zero mantissa means NaN + emitter.instruction("tbnz x9, #63, __rt_ftoa_neginf"); // negative sign means -INF + emit_ftoa_literal_aarch64(emitter, b"INF"); // +INF: "INF" + emitter.instruction("b __rt_ftoa_done"); // return after emitting positive infinity + emitter.label("__rt_ftoa_neginf"); + emit_ftoa_literal_aarch64(emitter, b"-INF"); // -INF: "-INF" + emitter.instruction("b __rt_ftoa_done"); // return after emitting negative infinity + emitter.label("__rt_ftoa_nan"); + emit_ftoa_literal_aarch64(emitter, b"NAN"); // NaN never carries a sign in PHP output + emitter.instruction("b __rt_ftoa_done"); // return after emitting the canonical NaN spelling // -- restore frame and return -- - emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #64"); // deallocate stack frame + emitter.label("__rt_ftoa_done"); + emitter.instruction("ldr x21, [sp, #88]"); // restore callee-saved register x21 + emitter.instruction("ldp x19, x20, [sp, #96]"); // restore callee-saved registers x19/x20 + emitter.instruction("ldp x29, x30, [sp, #112]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #128"); // deallocate stack frame emitter.instruction("ret"); // return to caller } -/// Emits the `__rt_ftoa` routine for Linux x86_64. +/// Writes a short non-finite literal (`"INF"`, `"-INF"`, or `"NAN"`) into +/// `_concat_buf` at the current `_concat_off`, publishes the advanced offset, and +/// leaves the `__rt_ftoa` AArch64 result (`x1` = pointer, `x2` = length) ready for +/// the caller to branch into the shared `__rt_ftoa_done` epilogue. +fn emit_ftoa_literal_aarch64(emitter: &mut Emitter, bytes: &[u8]) { + abi::emit_symbol_address(emitter, "x9", "_concat_off"); // resolve the concat-buffer cursor symbol + emitter.instruction("ldr x10, [x9]"); // load the current write offset + abi::emit_symbol_address(emitter, "x11", "_concat_buf"); // resolve the concat-buffer base symbol + emitter.instruction("add x1, x11, x10"); // result pointer = concat_buf + offset + for (i, byte) in bytes.iter().enumerate() { + emitter.instruction(&format!("mov w12, #{}", byte)); // load one literal byte + emitter.instruction(&format!("strb w12, [x1, #{}]", i)); // store the literal byte + } + emitter.instruction(&format!("mov x2, #{}", bytes.len())); // result length = literal byte count + emitter.instruction(&format!("add x10, x10, #{}", bytes.len())); // advance the offset past the literal + emitter.instruction("str x10, [x9]"); // publish the advanced offset +} + +/// Emits the `__rt_ftoa` routine for x86_64 (Linux, macOS, and windows-x86_64). /// /// # Input /// - `xmm0` holds the float value (SysV variadic ABI) @@ -87,44 +273,323 @@ pub fn emit_ftoa(emitter: &mut Emitter) { /// - `rax` = pointer to formatted string, `rdx` = length /// /// # Behavior -/// Same as `emit_ftoa` but for the Linux x86_64 target. Uses `rbp`-based -/// frame with 32 bytes of scratch space for concat cursor and output pointer. -/// Sets `eax = 1` to signal one SIMD register argument to `snprintf`. +/// Mirrors [`emit_ftoa`]'s AArch64 body exactly: an exponent-field bit test routes +/// non-finite inputs to a literal `"INF"`/`"-INF"`/`"NAN"` writer; finite inputs are +/// rounded to 14 significant digits with `snprintf("%.*e", 13, x)`, trimmed of +/// trailing zeros to emulate `zend_dtoa`'s mode-2 suppression, and laid out per +/// `zend_gcvt` (fixed-decimal for `-3 <= decpt <= 14`, exponential otherwise). +/// Every libc call is routed through [`Emitter::emit_call_c`] rather than `bl_c` so +/// windows-x86_64 reaches the registered `__rt_sys_snprintf`/`__rt_sys_strtol` +/// msvcrt shims instead of raw SysV-staged imports. fn emit_ftoa_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); - emitter.comment("--- runtime: ftoa ---"); + emitter.comment("--- runtime: ftoa (precision=14 zend_gcvt) ---"); emitter.label_global("__rt_ftoa"); emitter.instruction("push rbp"); // save the caller frame pointer before using stack locals emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the formatting helper - emitter.instruction("sub rsp, 32"); // reserve aligned scratch space for concat offsets and the output pointer - - crate::codegen_support::abi::emit_symbol_address(emitter, "r8", "_concat_off"); - emitter.instruction("mov r9, QWORD PTR [r8]"); // load the current concat cursor so formatted bytes append after prior output - emitter.instruction("mov QWORD PTR [rbp - 8], r9"); // save the original concat cursor for the final offset update - emitter.instruction("mov QWORD PTR [rbp - 16], r8"); // save the concat cursor symbol address for the final store - - crate::codegen_support::abi::emit_symbol_address(emitter, "r10", "_concat_buf"); - emitter.instruction("lea rdi, [r10 + r9]"); // compute the destination buffer inside the concat scratch area - emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // preserve the destination pointer for the return value - - emitter.instruction("mov esi, 32"); // cap float formatting to the same 32-byte scratch window used on AArch64 - crate::codegen_support::abi::emit_symbol_address(emitter, "rdx", "_fmt_g"); - emitter.instruction("mov eax, 1"); // SysV variadic ABI: one SIMD register is live for the double argument - emitter.instruction("call snprintf"); // format xmm0 using "%.14G" into the concat scratch buffer - - emitter.instruction("mov rdx, rax"); // return the formatted byte count in the string-length result register - emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // reload the concat cursor symbol address - emitter.instruction("mov r9, QWORD PTR [rbp - 8]"); // reload the original concat cursor - emitter.instruction("add r9, rdx"); // advance the concat cursor by the number of formatted bytes - emitter.instruction("mov QWORD PTR [r8], r9"); // publish the updated concat cursor for subsequent string writes - emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // return the pointer to the formatted float text - - emitter.instruction("add rsp, 32"); // release the local scratch area before returning + emitter.instruction("push rbx"); // save callee-saved rbx (p_eff) + emitter.instruction("push r12"); // save callee-saved r12 (sign flag) + emitter.instruction("push r13"); // save callee-saved r13 (exponent E) + emitter.instruction("push r14"); // save callee-saved r14 (exponential result start pointer) + emitter.instruction("sub rsp, 80"); // reserve scratch buffer + saved-double slot + emitter.instruction("movsd QWORD PTR [rsp + 64], xmm0"); // save the input double for re-formatting and compare + + // -- non-finite check: exponent field == 0x7FF means Inf or NaN -- + emitter.instruction("movq r9, xmm0"); // grab the raw bit pattern of the input double + emitter.instruction("mov r10, r9"); // copy the bits for exponent extraction + emitter.instruction("shr r10, 52"); // shift the exponent field into the low bits + emitter.instruction("and r10, 0x7ff"); // isolate the 11-bit exponent + emitter.instruction("cmp r10, 0x7ff"); // is the exponent all ones (Inf/NaN)? + emitter.instruction("je __rt_ftoa_nonfinite_x"); // dispatch to the INF/-INF/NAN literal writer + + // -- finite path: snprintf("%.*e", 13, x) -> 14 significant digits -- + emitter.instruction("lea rdi, [rsp]"); // snprintf buffer = scratch area + emitter.instruction("mov esi, 64"); // scratch buffer size + abi::emit_symbol_address(emitter, "rdx", "_fmt_star_e"); + emitter.instruction("mov ecx, 13"); // fixed precision: 14 significant digits (1 + 13 fractional) + emitter.instruction("movsd xmm0, QWORD PTR [rsp + 64]"); // reload the input double for the variadic call + emitter.instruction("mov eax, 1"); // one vector register used by the variadic call + emitter.emit_call_c("snprintf"); // format x at 14 significant digits into scratch + + // -- parse the sign from scratch[0] -- + emitter.instruction("movzx eax, BYTE PTR [rsp]"); // scratch[0] + emitter.instruction("cmp al, 45"); // is the first byte a '-' sign? + emitter.instruction("sete r12b"); // neg = 1 when the value is negative + emitter.instruction("movzx r12d, r12b"); // zero-extend the sign flag + + // -- parse the decimal exponent E: the 'e' marker sits at a fixed offset -- + // -- (neg + 1 digit + '.' + 13 frac digits) since precision is always 13 -- + emitter.instruction("lea rdi, [rsp + r12 + 16]"); // address of the exponent text after 'e' (sign + digits) + emitter.instruction("xor esi, esi"); // strtol endptr = NULL + emitter.instruction("mov edx, 10"); // base 10 + emitter.emit_call_c("strtol"); // E = parsed decimal exponent + emitter.instruction("mov r13, rax"); // keep E in a callee-saved register + + // -- trim trailing zero fractional digits to emulate zend_dtoa's mode-2 -- + // -- digit suppression; p_eff = number of significant fractional digits -- + emitter.instruction("mov ebx, 13"); // p_eff starts at the full 13 fractional digits + emitter.instruction("mov ecx, 12"); // j = index of the last fractional digit (0-based) + emitter.instruction("lea rsi, [rsp + r12 + 2]"); // frac digit base = scratch + neg + 2 + emitter.label("__rt_ftoa_trim_x"); + emitter.instruction("test rbx, rbx"); // nothing left to trim? + emitter.instruction("jz __rt_ftoa_trim_done_x"); // stop once every digit is trimmed + emitter.instruction("movzx eax, BYTE PTR [rsi + rcx]"); // load the fractional digit at index j + emitter.instruction("cmp al, 48"); // is it '0'? + emitter.instruction("jne __rt_ftoa_trim_done_x"); // stop at the first non-zero trailing digit + emitter.instruction("dec rbx"); // trim one more trailing zero + emitter.instruction("dec rcx"); // move to the previous fractional digit + emitter.instruction("jmp __rt_ftoa_trim_x"); // continue trimming + emitter.label("__rt_ftoa_trim_done_x"); + + // -- choose decimal vs exponential layout by decimal-point position -- + // -- (zend_gcvt: exponential when decpt > ndigit(14) or decpt < -3) -- + emitter.instruction("lea rax, [r13 + 1]"); // decpt = E + 1 + emitter.instruction("cmp rax, -3"); // compare decpt against -3 + emitter.instruction("jl __rt_ftoa_exp_x"); // decpt < -3 -> exponential form + emitter.instruction("cmp rax, 14"); // compare decpt against 14 + emitter.instruction("jg __rt_ftoa_exp_x"); // decpt > 14 -> exponential form + + // -- decimal form: snprintf("%.*f", max(0, p_eff - E), x) into concat_buf -- + emitter.instruction("mov rcx, rbx"); // fracdigits = p_eff ... + emitter.instruction("sub rcx, r13"); // ... minus E + emitter.instruction("test rcx, rcx"); // is the fractional digit count negative? + emitter.instruction("jns __rt_ftoa_frac_ok_x"); // non-negative count is fine + emitter.instruction("xor ecx, ecx"); // clamp to zero (integer-valued) + emitter.label("__rt_ftoa_frac_ok_x"); + abi::emit_load_symbol_to_reg(emitter, "r8", "_concat_off", 0); // current concat offset + abi::emit_symbol_address(emitter, "r9", "_concat_buf"); + emitter.instruction("lea rdi, [r9 + r8]"); // destination = concat_buf + offset + emitter.instruction("mov esi, 48"); // destination size cap + abi::emit_symbol_address(emitter, "rdx", "_fmt_star_f"); + emitter.instruction("movsd xmm0, QWORD PTR [rsp + 64]"); // reload the input double for the variadic call + emitter.instruction("mov eax, 1"); // one vector register used by the variadic call + emitter.emit_call_c("snprintf"); // format the decimal digits into concat_buf + emitter.instruction("mov rdx, rax"); // result length = bytes written + abi::emit_load_symbol_to_reg(emitter, "r8", "_concat_off", 0); // original offset (unchanged by snprintf) + abi::emit_symbol_address(emitter, "r9", "_concat_buf"); + emitter.instruction("lea rax, [r9 + r8]"); // result pointer = concat_buf + offset + emitter.instruction("add r8, rdx"); // advance the cursor past the digits + abi::emit_store_reg_to_symbol(emitter, "r8", "_concat_off", 0); // publish the new concat offset + emitter.instruction("jmp __rt_ftoa_done_x"); // finished decimal layout + + // -- exponential form: d.dddd...E[+-]exp with the leftover p_eff digits -- + emitter.label("__rt_ftoa_exp_x"); + abi::emit_load_symbol_to_reg(emitter, "r8", "_concat_off", 0); // current concat offset + abi::emit_symbol_address(emitter, "r9", "_concat_buf"); + emitter.instruction("lea r10, [r9 + r8]"); // cursor = concat_buf + offset + emitter.instruction("mov r14, r10"); // remember the result start pointer + emitter.instruction("test r12, r12"); // is the value negative? + emitter.instruction("jz __rt_ftoa_exp_first_x"); // skip sign when non-negative + emitter.instruction("mov BYTE PTR [r10], 45"); // emit '-' + emitter.instruction("inc r10"); // advance the cursor + emitter.label("__rt_ftoa_exp_first_x"); + emitter.instruction("movzx ecx, BYTE PTR [rsp + r12]"); // first significant digit (after optional sign) + emitter.instruction("mov BYTE PTR [r10], cl"); // emit the leading digit + emitter.instruction("inc r10"); // advance the cursor + emitter.instruction("mov BYTE PTR [r10], 46"); // emit '.' + emitter.instruction("inc r10"); // advance the cursor + emitter.instruction("test rbx, rbx"); // does the mantissa have fractional digits? + emitter.instruction("jnz __rt_ftoa_exp_frac_x"); // p_eff>0 copies the fractional digits + emitter.instruction("mov BYTE PTR [r10], 48"); // emit '0' (mantissa "d.0") + emitter.instruction("inc r10"); // advance the cursor + emitter.instruction("jmp __rt_ftoa_exp_e_x"); // continue with the exponent marker + emitter.label("__rt_ftoa_exp_frac_x"); + emitter.instruction("lea rsi, [rsp + r12 + 2]"); // &scratch[neg+2] = first fractional digit + emitter.instruction("xor edi, edi"); // fractional copy index + emitter.label("__rt_ftoa_exp_frac_loop_x"); + emitter.instruction("cmp rdi, rbx"); // copied all p_eff fractional digits? + emitter.instruction("jge __rt_ftoa_exp_e_x"); // mantissa fraction complete + emitter.instruction("movzx ecx, BYTE PTR [rsi + rdi]"); // load fractional digit + emitter.instruction("mov BYTE PTR [r10], cl"); // emit fractional digit + emitter.instruction("inc r10"); // advance the cursor + emitter.instruction("inc rdi"); // advance the fractional index + emitter.instruction("jmp __rt_ftoa_exp_frac_loop_x"); // copy the next fractional digit + emitter.label("__rt_ftoa_exp_e_x"); + emitter.instruction("mov BYTE PTR [r10], 69"); // ASCII 'E' (precision-14 layout is always uppercase) + emitter.instruction("inc r10"); // advance the cursor + emitter.instruction("test r13, r13"); // is the exponent negative? + emitter.instruction("jns __rt_ftoa_exp_pos_x"); // non-negative exponent uses '+' + emitter.instruction("mov BYTE PTR [r10], 45"); // emit '-' + emitter.instruction("inc r10"); // advance the cursor + emitter.instruction("neg r13"); // make the exponent magnitude positive + emitter.instruction("jmp __rt_ftoa_exp_mag_x"); // emit the magnitude digits + emitter.label("__rt_ftoa_exp_pos_x"); + emitter.instruction("mov BYTE PTR [r10], 43"); // emit '+' + emitter.instruction("inc r10"); // advance the cursor + emitter.label("__rt_ftoa_exp_mag_x"); + emitter.instruction("mov rax, r13"); // exponent magnitude into the dividend + emitter.instruction("xor edx, edx"); // clear the high dividend half + emitter.instruction("mov ecx, 100"); // divisor for the hundreds digit + emitter.instruction("div rcx"); // rax = E/100 (hundreds), rdx = E%100 + emitter.instruction("mov r8, rax"); // save the hundreds digit + emitter.instruction("mov rax, rdx"); // remainder (E % 100) into the dividend + emitter.instruction("xor edx, edx"); // clear the high dividend half + emitter.instruction("mov ecx, 10"); // divisor for the tens digit + emitter.instruction("div rcx"); // rax = tens, rdx = ones + emitter.instruction("mov r9, rax"); // save the tens digit + emitter.instruction("mov r11, rdx"); // save the ones digit + emitter.instruction("test r8, r8"); // is the hundreds digit zero? + emitter.instruction("jz __rt_ftoa_exp_tens_x"); // skip leading-zero hundreds + emitter.instruction("lea ecx, [r8 + 48]"); // hundreds digit to ASCII + emitter.instruction("mov BYTE PTR [r10], cl"); // emit hundreds digit + emitter.instruction("inc r10"); // advance the cursor + emitter.instruction("lea ecx, [r9 + 48]"); // tens digit to ASCII + emitter.instruction("mov BYTE PTR [r10], cl"); // emit tens digit + emitter.instruction("inc r10"); // advance the cursor + emitter.instruction("jmp __rt_ftoa_exp_ones_x"); // emit the ones digit + emitter.label("__rt_ftoa_exp_tens_x"); + emitter.instruction("test r9, r9"); // is the tens digit zero? + emitter.instruction("jz __rt_ftoa_exp_ones_x"); // skip leading-zero tens + emitter.instruction("lea ecx, [r9 + 48]"); // tens digit to ASCII + emitter.instruction("mov BYTE PTR [r10], cl"); // emit tens digit + emitter.instruction("inc r10"); // advance the cursor + emitter.label("__rt_ftoa_exp_ones_x"); + emitter.instruction("lea ecx, [r11 + 48]"); // ones digit to ASCII + emitter.instruction("mov BYTE PTR [r10], cl"); // emit ones digit + emitter.instruction("inc r10"); // advance the cursor + emitter.instruction("mov rax, r14"); // result pointer = start + emitter.instruction("mov rdx, r10"); // cursor (one past the last byte) + emitter.instruction("sub rdx, rax"); // result length = cursor - start + abi::emit_load_symbol_to_reg(emitter, "r8", "_concat_off", 0); // original concat offset + emitter.instruction("add r8, rdx"); // advance past the emitted bytes + abi::emit_store_reg_to_symbol(emitter, "r8", "_concat_off", 0); // publish the new concat offset + emitter.instruction("jmp __rt_ftoa_done_x"); // finished exponential layout + + // -- non-finite: INF / -INF / NAN, matching __rt_serialize's classification -- + emitter.label("__rt_ftoa_nonfinite_x"); + emitter.instruction("mov r10, r9"); // copy the bits for mantissa testing + emitter.instruction("shl r10, 12"); // drop the sign+exponent to test the mantissa + emitter.instruction("jnz __rt_ftoa_nan_x"); // non-zero mantissa means NaN + emitter.instruction("bt r9, 63"); // test the float sign bit + emitter.instruction("jc __rt_ftoa_neginf_x"); // negative sign means -INF + emit_ftoa_literal_x86_64(emitter, b"INF"); // +INF: "INF" + emitter.instruction("jmp __rt_ftoa_done_x"); // return after emitting positive infinity + emitter.label("__rt_ftoa_neginf_x"); + emit_ftoa_literal_x86_64(emitter, b"-INF"); // -INF: "-INF" + emitter.instruction("jmp __rt_ftoa_done_x"); // return after emitting negative infinity + emitter.label("__rt_ftoa_nan_x"); + emit_ftoa_literal_x86_64(emitter, b"NAN"); // NaN never carries a sign in PHP output + emitter.instruction("jmp __rt_ftoa_done_x"); // return after emitting the canonical NaN spelling + + emitter.label("__rt_ftoa_done_x"); + emitter.instruction("add rsp, 80"); // release the local scratch area before returning + emitter.instruction("pop r14"); // restore callee-saved r14 + emitter.instruction("pop r13"); // restore callee-saved r13 + emitter.instruction("pop r12"); // restore callee-saved r12 + emitter.instruction("pop rbx"); // restore callee-saved rbx emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return pointer+length in rax/rdx } +/// Writes a short non-finite literal (`"INF"`, `"-INF"`, or `"NAN"`) into +/// `_concat_buf` at the current `_concat_off`, publishes the advanced offset, and +/// leaves the `__rt_ftoa` x86_64 result (`rax` = pointer, `rdx` = length) ready +/// for the caller to branch into the shared `__rt_ftoa_done_x` epilogue. +fn emit_ftoa_literal_x86_64(emitter: &mut Emitter, bytes: &[u8]) { + abi::emit_load_symbol_to_reg(emitter, "r10", "_concat_off", 0); // load the current write offset + abi::emit_symbol_address(emitter, "r9", "_concat_buf"); // resolve the concat-buffer base symbol + emitter.instruction("lea rax, [r9 + r10]"); // result pointer = concat_buf + offset + for (i, byte) in bytes.iter().enumerate() { + emitter.instruction(&format!("mov BYTE PTR [rax + {}], {}", i, byte)); // store one literal byte + } + emitter.instruction(&format!("mov rdx, {}", bytes.len())); // result length = literal byte count + emitter.instruction(&format!("add r10, {}", bytes.len())); // advance the offset past the literal + abi::emit_store_reg_to_symbol(emitter, "r10", "_concat_off", 0); // publish the advanced offset +} + +/// Converts a double-precision float to a PHP-compatible byte string at PHP's +/// `serialize_precision = -1` (shortest round-trip): the precision `var_dump` +/// uses, shared with `json_encode`/`serialize`/`var_export` — NOT the +/// `precision = 14` setting [`emit_ftoa`] implements for `echo`/`(string)`. +/// +/// # Input +/// - ARM64: `d0` holds the float value +/// - x86_64: `xmm0` holds the float value (SysV variadic ABI) +/// +/// # Output +/// - ARM64: `x1` = pointer to string, `x2` = length +/// - x86_64: `rax` = pointer to string, `rdx` = length +/// +/// # Behavior +/// `__rt_json_ftoa` assumes its caller has already excluded non-finite values +/// (true of its other two callers, `__rt_json_encode_float` and +/// `__rt_serialize`'s float case, which both branch around it for Inf/NaN); a +/// bare `var_dump($inf)` has no such caller, so this wrapper repeats the +/// exponent-field bit test from [`emit_ftoa`]/`__rt_serialize` to emit PHP's +/// `"INF"`/`"-INF"`/`"NAN"` spellings directly, and tail-calls into +/// `__rt_json_ftoa` (uppercase `'E'` marker, matching `serialize`'s layout) +/// for every finite input — no stack frame of its own is needed since it never +/// has cleanup to run after the delegated call returns. +pub fn emit_var_dump_ftoa(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_var_dump_ftoa_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: var_dump_ftoa (serialize_precision=-1, Inf/NaN-safe) ---"); + emitter.label_global("__rt_var_dump_ftoa"); + + // -- non-finite check: exponent field == 0x7FF means Inf or NaN -- + emitter.instruction("fmov x9, d0"); // grab the raw bit pattern of the input double + emitter.instruction("lsr x10, x9, #52"); // shift the exponent field into the low bits + emitter.instruction("and x10, x10, #0x7ff"); // isolate the 11-bit exponent + emitter.instruction("cmp x10, #0x7ff"); // is the exponent all ones (Inf/NaN)? + emitter.instruction("b.ne __rt_var_dump_ftoa_finite"); // finite values delegate to __rt_json_ftoa + emitter.instruction("lsl x10, x9, #12"); // drop the sign+exponent to test the mantissa + emitter.instruction("cbnz x10, __rt_var_dump_ftoa_nan"); // non-zero mantissa means NaN + emitter.instruction("tbnz x9, #63, __rt_var_dump_ftoa_neginf"); // negative sign means -INF + emit_ftoa_literal_aarch64(emitter, b"INF"); // +INF: "INF" + emitter.instruction("ret"); // return after emitting positive infinity + emitter.label("__rt_var_dump_ftoa_neginf"); + emit_ftoa_literal_aarch64(emitter, b"-INF"); // -INF: "-INF" + emitter.instruction("ret"); // return after emitting negative infinity + emitter.label("__rt_var_dump_ftoa_nan"); + emit_ftoa_literal_aarch64(emitter, b"NAN"); // NaN never carries a sign in PHP output + emitter.instruction("ret"); // return after emitting the canonical NaN spelling + + // -- finite: tail-call __rt_json_ftoa with the 'E' (serialize) marker -- + emitter.label("__rt_var_dump_ftoa_finite"); + emitter.instruction("mov w0, #69"); // exponent marker 'E' (serialize_precision=-1 layout) + emitter.instruction("b __rt_json_ftoa"); // tail call: its ret returns straight to our caller +} + +/// Emits the x86_64 variant of `__rt_var_dump_ftoa` (Linux, macOS, and windows-x86_64). +/// Mirrors [`emit_var_dump_ftoa`]'s AArch64 body exactly; see that function's doc +/// comment for the Inf/NaN rationale. +fn emit_var_dump_ftoa_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: var_dump_ftoa (serialize_precision=-1, Inf/NaN-safe) ---"); + emitter.label_global("__rt_var_dump_ftoa"); + + // -- non-finite check: exponent field == 0x7FF means Inf or NaN -- + emitter.instruction("movq r9, xmm0"); // grab the raw bit pattern of the input double + emitter.instruction("mov r10, r9"); // copy the bits for exponent extraction + emitter.instruction("shr r10, 52"); // shift the exponent field into the low bits + emitter.instruction("and r10, 0x7ff"); // isolate the 11-bit exponent + emitter.instruction("cmp r10, 0x7ff"); // is the exponent all ones (Inf/NaN)? + emitter.instruction("jne __rt_var_dump_ftoa_finite_x"); // finite values delegate to __rt_json_ftoa + emitter.instruction("mov r10, r9"); // copy the bits for mantissa testing + emitter.instruction("shl r10, 12"); // drop the sign+exponent to test the mantissa + emitter.instruction("jnz __rt_var_dump_ftoa_nan_x"); // non-zero mantissa means NaN + emitter.instruction("bt r9, 63"); // test the float sign bit + emitter.instruction("jc __rt_var_dump_ftoa_neginf_x"); // negative sign means -INF + emit_ftoa_literal_x86_64(emitter, b"INF"); // +INF: "INF" + emitter.instruction("ret"); // return after emitting positive infinity + emitter.label("__rt_var_dump_ftoa_neginf_x"); + emit_ftoa_literal_x86_64(emitter, b"-INF"); // -INF: "-INF" + emitter.instruction("ret"); // return after emitting negative infinity + emitter.label("__rt_var_dump_ftoa_nan_x"); + emit_ftoa_literal_x86_64(emitter, b"NAN"); // NaN never carries a sign in PHP output + emitter.instruction("ret"); // return after emitting the canonical NaN spelling + + // -- finite: tail-call __rt_json_ftoa with the 'E' (serialize) marker -- + emitter.label("__rt_var_dump_ftoa_finite_x"); + emitter.instruction("mov edi, 69"); // exponent marker 'E' (serialize_precision=-1 layout) + emitter.instruction("jmp __rt_json_ftoa"); // tail call: its ret returns straight to our caller +} + #[cfg(test)] mod tests { use crate::codegen_support::platform::{Arch, Platform, Target}; @@ -144,4 +609,38 @@ mod tests { assert!(asm.contains("call snprintf\n")); assert!(asm.contains("mov rdx, rax\n")); } + + /// Verifies the php_gcvt exponential-vs-fixed threshold constant (`decpt > 14`) + /// and the fixed 14-significant-digit precision are present in the AArch64 body, + /// pinning the values verified against php-src's `zend_gcvt` (not `json_ftoa`'s + /// `> 17` shortest-round-trip threshold). + #[test] + fn test_emit_ftoa_aarch64_uses_precision_14_threshold() { + let mut emitter = Emitter::new(Target::new(Platform::MacOS, Arch::AArch64)); + emit_ftoa(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains("mov x19, #13\n")); + assert!(asm.contains("cmp x9, #14\n")); + assert!(asm.contains("cmn x9, #3\n")); + assert!(asm.contains("mov w13, #69\n")); + } + + /// Verifies non-finite inputs are classified via the exponent/mantissa/sign bit + /// tests (mirroring `__rt_serialize`'s proven float special-casing) rather than + /// falling through to `snprintf("%.14G", ...)`, on both architectures. + #[test] + fn test_emit_ftoa_detects_non_finite_on_both_arches() { + let mut aarch64 = Emitter::new(Target::new(Platform::MacOS, Arch::AArch64)); + emit_ftoa(&mut aarch64); + let aarch64_asm = aarch64.output(); + assert!(aarch64_asm.contains("__rt_ftoa_nonfinite:\n")); + assert!(aarch64_asm.contains("tbnz x9, #63, __rt_ftoa_neginf\n")); + + let mut x86 = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_ftoa(&mut x86); + let x86_asm = x86.output(); + assert!(x86_asm.contains("__rt_ftoa_nonfinite_x:\n")); + assert!(x86_asm.contains("bt r9, 63\n")); + } } diff --git a/src/codegen_support/runtime/strings/hash.rs b/src/codegen_support/runtime/strings/hash.rs index 072a56c885..04677aaeda 100644 --- a/src/codegen_support/runtime/strings/hash.rs +++ b/src/codegen_support/runtime/strings/hash.rs @@ -122,7 +122,7 @@ fn emit_hash_linux_x86_64(emitter: &mut Emitter) { abi::emit_load_symbol_to_reg(emitter, "r9", "_elephc_crypto_hash_fn", 0); // load the published elephc_crypto_hash function pointer emitter.instruction("test r9, r9"); // a null slot means the program never linked elephc-crypto → unknown algo emitter.instruction("jz __rt_hash_unknown_linux_x86_64"); // throw the unknown-algorithm ValueError when the slot is null - emitter.instruction("call r9"); // compute the raw digest into the stack buffer; rax = digest length or -1 + emitter.emit_native_bridge_call("r9", 5); // compute the raw digest into the stack buffer; rax = digest length or -1 // -- handle an unknown algorithm (-1) before formatting -- emitter.instruction("test rax, rax"); // did elephc_crypto_hash reject the algorithm name? @@ -147,3 +147,61 @@ fn emit_hash_linux_x86_64(emitter: &mut Emitter) { HASH_UNKNOWN_ALGO_MSG.len(), ); } + +#[cfg(test)] +mod tests { + use crate::codegen_support::emit::Emitter; + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::*; + + /// Verifies the windows-x86_64 `__rt_hash` indirect call into + /// `elephc_crypto_hash` — a REAL native (MSx64-ABI) function reached + /// through the `_elephc_crypto_hash_fn` pointer slot — goes through the + /// F46 native-bridge shim instead of a bare `call r9`: the fn-ptr is + /// relocated to r11, the 5-int-arg call reserves 48 bytes (32-byte MSx64 + /// shadow space + one 8-byte slot for the 5th SysV arg, 16-byte aligned), + /// the 5th arg (r8, the digest output buffer pointer) is staged onto the + /// MSx64 stack BEFORE the remap's `mov r8, rdx` clobbers r8, and the + /// first 4 SysV args are remapped into rcx/rdx/r8/r9 before the call. + /// Without this, elephc_crypto_hash (compiled to the MSx64 ABI on + /// windows) would read its 5 arguments from the wrong registers. + #[test] + fn test_windows_x86_64_hash_native_bridge_call_stages_5th_arg_before_remap() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_hash(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains("mov r11, r9"), "expected the fn-ptr relocated off the MSx64 arg registers"); + assert!(asm.contains("sub rsp, 48"), "expected 32-byte shadow + one 8-byte 5th-arg slot"); + let stage_idx = asm + .find("mov QWORD PTR [rsp + 32], r8") + .expect("expected the 5th SysV int arg staged to the MSx64 stack slot"); + let remap_idx = asm + .find("mov r8, rdx") + .expect("expected the remap's mov r8, rdx (MSx64 arg2 <- SysV arg2)"); + assert!(stage_idx < remap_idx, "the 5th-arg stack stage must precede the remap's mov r8, rdx clobber"); + assert!(asm.contains("mov rcx, rdi"), "expected MSx64 arg0 <- SysV arg0"); + assert!(asm.contains("mov rdx, rsi"), "expected MSx64 arg1 <- SysV arg1"); + assert!(asm.contains("mov r9, rcx"), "expected MSx64 arg3 <- SysV arg3"); + assert!(asm.contains("call r11"), "expected the indirect call through the relocated pointer"); + assert!(asm.contains("add rsp, 48"), "expected the shadow + stack-arg scratch released"); + assert!(!asm.contains("call r9\n"), "must not leave a bare call r9 (F46: MSx64 callee reading SysV registers)"); + } + + /// Verifies linux-x86_64 emission for `__rt_hash` stays byte-identical to + /// before the F46 native-bridge shim was introduced: the MSx64 relocation + /// and shadow-space staging are windows-x86_64-only, so linux-x86_64 must + /// keep the plain bare `call r9` into `elephc_crypto_hash`. + #[test] + fn test_linux_x86_64_hash_call_stays_bare_call_r9() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_hash(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains(" call r9\n"), "expected the byte-identical bare call r9"); + assert!(!asm.contains("mov r11"), "linux-x86_64 must not relocate the fn-ptr"); + assert!(!asm.contains("sub rsp, 48"), "linux-x86_64 must not reserve MSx64 shadow space"); + assert!(!asm.contains("call r11")); + } +} diff --git a/src/codegen_support/runtime/strings/hash_context.rs b/src/codegen_support/runtime/strings/hash_context.rs index 2f16c14ad6..bb658c0ddd 100644 --- a/src/codegen_support/runtime/strings/hash_context.rs +++ b/src/codegen_support/runtime/strings/hash_context.rs @@ -105,7 +105,7 @@ fn emit_hash_init(emitter: &mut Emitter) { emitter.instruction("jz __rt_hash_init_unknown_x86"); // jump to the ValueError throw when unavailable - emitter.instruction("call r9"); // create the context, rax = handle (null on unknown algo) + emitter.emit_native_bridge_call("r9", 2); // create the context, rax = handle (null on unknown algo) emitter.instruction("test rax, rax"); // null handle = unknown algorithm @@ -183,7 +183,7 @@ fn emit_hash_update(emitter: &mut Emitter) { emitter.instruction("jz __rt_hash_update_done_x86"); // jump past the call when unavailable - emitter.instruction("call r9"); // elephc_crypto_update(ctx, data_ptr, data_len) + emitter.emit_native_bridge_call("r9", 3); // elephc_crypto_update(ctx, data_ptr, data_len) emitter.label("__rt_hash_update_done_x86"); emitter.instruction("mov eax, 1"); // hash_update() returns true @@ -264,7 +264,7 @@ fn emit_hash_final(emitter: &mut Emitter) { emitter.instruction("jz __rt_hash_final_empty_x86"); // skip the call when the runtime is unavailable - emitter.instruction("call r9"); // finalize+free the context, rax = raw digest length + emitter.emit_native_bridge_call("r9", 2); // finalize+free the context, rax = raw digest length emitter.instruction("jmp __rt_hash_final_len_x86"); // proceed with the real digest length @@ -341,7 +341,7 @@ fn emit_hash_copy(emitter: &mut Emitter) { emitter.instruction("jz __rt_hash_copy_done_x86"); // skip the clone call when unavailable - emitter.instruction("call r9"); // clone the context, rax = new handle + emitter.emit_native_bridge_call("r9", 1); // clone the context, rax = new handle emitter.label("__rt_hash_copy_done_x86"); emitter.instruction("mov rdi, rax"); // Mixed payload = the cloned context handle @@ -410,7 +410,7 @@ fn emit_hash_free(emitter: &mut Emitter) { emitter.instruction("jz __rt_hash_ctx_free_done_x86"); // jump to the return path when unavailable - emitter.instruction("call r9"); // elephc_crypto_free(ctx) — frees the context + emitter.emit_native_bridge_call("r9", 1); // elephc_crypto_free(ctx) — frees the context emitter.label("__rt_hash_ctx_free_done_x86"); emitter.instruction("mov rsp, rbp"); // release the frame @@ -422,3 +422,51 @@ fn emit_hash_free(emitter: &mut Emitter) { } } } + +#[cfg(test)] +mod tests { + use crate::codegen_support::emit::Emitter; + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::*; + + /// Verifies the windows-x86_64 `__rt_hash_init` indirect call into + /// `elephc_crypto_init` — a REAL native (MSx64-ABI) function reached + /// through the `_elephc_crypto_init_fn` pointer slot — goes through the + /// F46 native-bridge shim instead of a bare `call r9`: the fn-ptr is + /// relocated to r11, the 32-byte MSx64 shadow space is reserved, the two + /// SysV args (rdi=name ptr, rsi=name len) are remapped into rcx/rdx, and + /// the call goes through r11. Without this, elephc_crypto_init (compiled + /// to the MSx64 ABI on windows) would read its arguments from the wrong + /// registers — the root cause of the F46 hash_*/md5_*/sha1_* cluster. + #[test] + fn test_windows_x86_64_hash_init_native_bridge_call_remaps_two_args() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_hash_context(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains("mov r11, r9"), "expected the fn-ptr relocated off the MSx64 arg registers"); + assert!(asm.contains("sub rsp, 32"), "expected the 32-byte MSx64 shadow space reserved"); + assert!(asm.contains("mov rcx, rdi"), "expected MSx64 arg0 <- SysV arg0 (algorithm name ptr)"); + assert!(asm.contains("mov rdx, rsi"), "expected MSx64 arg1 <- SysV arg1 (algorithm name len)"); + assert!(asm.contains("call r11"), "expected the indirect call through the relocated pointer"); + assert!(asm.contains("add rsp, 32"), "expected the shadow-space scratch released"); + } + + /// Verifies linux-x86_64 emission for `__rt_hash_init` (and the other + /// HashContext helpers) stays byte-identical to before the F46 + /// native-bridge shim was introduced: the MSx64 relocation and + /// shadow-space staging are windows-x86_64-only, so linux-x86_64 must + /// keep the plain bare `call r9` sequence into elephc-crypto. + #[test] + fn test_linux_x86_64_hash_context_calls_stay_bare_call_r9() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_hash_context(&mut emitter); + let asm = emitter.output(); + + assert_eq!(asm.matches("call r9").count(), 5, "expected all 5 HashContext call sites to stay bare call r9"); + assert!(!asm.contains("mov r11"), "linux-x86_64 must not relocate any fn-ptr"); + assert!(!asm.contains("call r11")); + assert!(!asm.contains("sub rsp, 32"), "linux-x86_64 must not reserve MSx64 shadow space"); + } +} diff --git a/src/codegen_support/runtime/strings/hash_hmac.rs b/src/codegen_support/runtime/strings/hash_hmac.rs index f3584b3aff..75a3d3d843 100644 --- a/src/codegen_support/runtime/strings/hash_hmac.rs +++ b/src/codegen_support/runtime/strings/hash_hmac.rs @@ -20,7 +20,7 @@ use crate::codegen_support::abi; use crate::codegen_support::hash_crypto; use crate::codegen_support::emit::Emitter; -use crate::codegen_support::platform::Arch; +use crate::codegen_support::platform::{Arch, Platform}; use crate::codegen_support::runtime::data::HASH_HMAC_UNKNOWN_ALGO_MSG; /// Emits the `__rt_hash_hmac` runtime helper for the `hash_hmac()` built-in. @@ -148,10 +148,21 @@ fn emit_hash_hmac_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("test rax, rax"); // a null slot means the program never linked elephc-crypto → unknown algo emitter.instruction("jz __rt_hash_hmac_unknown_linux_x86_64"); // throw the unknown-algorithm ValueError when the slot is null emitter.instruction("lea r10, [rbp - 64]"); // address of the stack-backed 64-byte raw-digest output buffer - emitter.instruction("sub rsp, 16"); // reserve a 16-byte stack-arg slot for the 7th int arg (rsp stays 16-aligned) - emitter.instruction("mov QWORD PTR [rsp], r10"); // 7th arg (output buffer address) at [rsp+0] - emitter.instruction("call rax"); // compute the raw HMAC into the stack buffer; rax = digest length or -1 - emitter.instruction("add rsp, 16"); // release the stack-arg slot + if emitter.target.platform == Platform::Windows && emitter.target.arch == Arch::X86_64 { + emitter.instruction("mov r11, rax"); // relocate fn-ptr off the MSx64 arg registers + emitter.instruction("sub rsp, 64"); // 32-byte shadow + three 8-byte stack-arg slots (16-aligned) + emitter.instruction("mov QWORD PTR [rsp + 32], r8"); // MSx64 5th arg = data_ptr (SysV r8) — save before the remap clobbers r8 + emitter.instruction("mov QWORD PTR [rsp + 40], r9"); // MSx64 6th arg = data_len (SysV r9) — save before the remap clobbers r9 + emitter.instruction("mov QWORD PTR [rsp + 48], r10"); // MSx64 7th arg = output-buffer address (already in r10) + emitter.remap_sysv_args_to_platform_for_callback(4); // args 0-3 SysV→MSx64 (rcx←rdi, rdx←rsi, r8←rdx, r9←rcx), reverse order + emitter.instruction("call r11"); // invoke elephc_crypto_hmac via the relocated pointer (MSx64 ABI) + emitter.instruction("add rsp, 64"); // release shadow + stack-arg scratch + } else { + emitter.instruction("sub rsp, 16"); // reserve a 16-byte stack-arg slot for the 7th int arg (rsp stays 16-aligned) + emitter.instruction("mov QWORD PTR [rsp], r10"); // 7th arg (output buffer address) at [rsp+0] + emitter.instruction("call rax"); // compute the raw HMAC into the stack buffer; rax = digest length or -1 + emitter.instruction("add rsp, 16"); // release the stack-arg slot + } // -- handle an unknown algorithm / non-crypto checksum (-1) before formatting -- emitter.instruction("test rax, rax"); // did elephc_crypto_hmac reject the algorithm name? @@ -176,3 +187,74 @@ fn emit_hash_hmac_linux_x86_64(emitter: &mut Emitter) { HASH_HMAC_UNKNOWN_ALGO_MSG.len(), ); } + +#[cfg(test)] +mod tests { + use crate::codegen_support::emit::Emitter; + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::*; + + /// Verifies the windows-x86_64 `__rt_hash_hmac` indirect call into + /// `elephc_crypto_hmac` — a REAL native (MSx64-ABI) function reached + /// through the `_elephc_crypto_hmac_fn` pointer slot — goes through the + /// bespoke F46 7-int-arg native-bridge shim instead of a bare `call rax`: + /// the fn-ptr is relocated to r11, 64 bytes are reserved (32-byte MSx64 + /// shadow space + three 8-byte slots for the 5th/6th/7th SysV int args, + /// 16-byte aligned), the 5th/6th/7th args (r8/r9/r10: data_ptr, data_len, + /// the digest output buffer pointer) are staged onto the MSx64 stack + /// BEFORE the remap's `mov r8, rdx` clobbers r8, and the first 4 SysV + /// args are remapped into rcx/rdx/r8/r9 before the call. Without this, + /// elephc_crypto_hmac (compiled to the MSx64 ABI on windows) would read + /// its 7 arguments from the wrong registers. + #[test] + fn test_windows_x86_64_hash_hmac_native_bridge_call_stages_7th_arg_before_remap() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_hash_hmac(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains("mov r11, rax"), "expected the fn-ptr relocated off the MSx64 arg registers"); + assert!(asm.contains("sub rsp, 64"), "expected 32-byte shadow + three 8-byte 5th/6th/7th-arg slots"); + let stage_r8_idx = asm + .find("mov QWORD PTR [rsp + 32], r8") + .expect("expected the 5th SysV int arg (data_ptr) staged to the MSx64 stack slot"); + let stage_r9_idx = asm + .find("mov QWORD PTR [rsp + 40], r9") + .expect("expected the 6th SysV int arg (data_len) staged to the MSx64 stack slot"); + let stage_r10_idx = asm + .find("mov QWORD PTR [rsp + 48], r10") + .expect("expected the 7th arg (output buffer address) staged to the MSx64 stack slot"); + let remap_idx = asm + .find("mov r8, rdx") + .expect("expected the remap's mov r8, rdx (MSx64 arg2 <- SysV arg2)"); + assert!(stage_r8_idx < remap_idx, "the 5th-arg stack stage must precede the remap's mov r8, rdx clobber"); + assert!(stage_r9_idx < remap_idx, "the 6th-arg stack stage must precede the remap's mov r8, rdx clobber"); + assert!(stage_r10_idx < remap_idx, "the 7th-arg stack stage must precede the remap's mov r8, rdx clobber"); + assert!(asm.contains("mov rcx, rdi"), "expected MSx64 arg0 <- SysV arg0"); + assert!(asm.contains("mov rdx, rsi"), "expected MSx64 arg1 <- SysV arg1"); + assert!(asm.contains("mov r9, rcx"), "expected MSx64 arg3 <- SysV arg3"); + assert!(asm.contains("call r11"), "expected the indirect call through the relocated pointer"); + assert!(asm.contains("add rsp, 64"), "expected the shadow + stack-arg scratch released"); + assert!(!asm.contains("call rax\n"), "must not leave a bare call rax (F46: MSx64 callee reading SysV registers)"); + } + + /// Verifies linux-x86_64 emission for `__rt_hash_hmac` stays byte-identical + /// to before the F46 native-bridge shim was introduced: the MSx64 + /// relocation and shadow-space staging are windows-x86_64-only, so + /// linux-x86_64 must keep the plain bare `call rax` into + /// `elephc_crypto_hmac` with the original 16-byte stack-arg slot. + #[test] + fn test_linux_x86_64_hash_hmac_call_stays_bare_call_rax() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_hash_hmac(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains(" sub rsp, 16\n"), "expected the byte-identical 16-byte stack-arg slot reservation"); + assert!(asm.contains(" mov QWORD PTR [rsp], r10\n"), "expected the byte-identical 7th-arg stack store"); + assert!(asm.contains(" call rax\n"), "expected the byte-identical bare call rax"); + assert!(asm.contains(" add rsp, 16\n"), "expected the byte-identical stack-arg slot release"); + assert!(!asm.contains("mov r11"), "linux-x86_64 must not relocate the fn-ptr"); + assert!(!asm.contains("sub rsp, 64"), "linux-x86_64 must not reserve MSx64 shadow space"); + assert!(!asm.contains("call r11")); + } +} diff --git a/src/codegen_support/runtime/strings/mb_strlen.rs b/src/codegen_support/runtime/strings/mb_strlen.rs index b6687390a7..080463319e 100644 --- a/src/codegen_support/runtime/strings/mb_strlen.rs +++ b/src/codegen_support/runtime/strings/mb_strlen.rs @@ -352,6 +352,7 @@ fn emit_mb_strlen_x86_64(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // iconv input pointer variable starts at the PHP string bytes emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // iconv input byte count variable starts at the PHP string length emitter.instruction("mov QWORD PTR [rbp - 24], 0"); // decoded character count starts at zero + emitter.instruction("mov QWORD PTR [rbp - 64], r9"); // preserve the explicit encoding-name length across helper calls // -- copy the length-delimited PHP encoding name into a stack C string -- emitter.instruction("lea rdi, [rbp - 160]"); // destination is the 64-byte encoding-name buffer @@ -368,35 +369,45 @@ fn emit_mb_strlen_x86_64(emitter: &mut Emitter) { // -- fast-path PHP's default UTF-8 names and byte-count encodings -- emitter.instruction("lea rdi, [rbp - 160]"); // first strcasecmp argument is the copied encoding name - abi::emit_symbol_address(emitter, "rsi", "_mb_strlen_utf8_name"); - emitter.instruction("call strcasecmp"); // compare the explicit encoding with UTF-8 case-insensitively + emitter.instruction("mov rsi, QWORD PTR [rbp - 64]"); // first string length is the explicit encoding-name length + abi::emit_symbol_address(emitter, "rdx", "_mb_strlen_utf8_name"); + emitter.instruction("mov rcx, 5"); // UTF-8 literal length + emitter.instruction("call __rt_strcasecmp"); // compare the explicit encoding with UTF-8 case-insensitively emitter.instruction("test eax, eax"); // did the encoding match UTF-8? emitter.instruction("jz __rt_mb_strlen_use_utf8_framed_x86"); // UTF-8 uses the allocation-free validated scanner emitter.instruction("lea rdi, [rbp - 160]"); // reload the copied encoding name after strcasecmp - abi::emit_symbol_address(emitter, "rsi", "_mb_strlen_utf8_alias"); - emitter.instruction("call strcasecmp"); // compare the explicit encoding with PHP's UTF8 alias + emitter.instruction("mov rsi, QWORD PTR [rbp - 64]"); // reload the explicit encoding-name length + abi::emit_symbol_address(emitter, "rdx", "_mb_strlen_utf8_alias"); + emitter.instruction("mov rcx, 4"); // UTF8 alias literal length + emitter.instruction("call __rt_strcasecmp"); // compare the explicit encoding with PHP's UTF8 alias emitter.instruction("test eax, eax"); // did the encoding match UTF8? emitter.instruction("jz __rt_mb_strlen_use_utf8_framed_x86"); // the UTF8 alias uses the same validated scanner emitter.instruction("lea rdi, [rbp - 160]"); // reload the copied encoding name for the byte-count aliases - abi::emit_symbol_address(emitter, "rsi", "_mb_strlen_8bit_name"); - emitter.instruction("call strcasecmp"); // compare the explicit encoding with 8bit + emitter.instruction("mov rsi, QWORD PTR [rbp - 64]"); // reload the explicit encoding-name length + abi::emit_symbol_address(emitter, "rdx", "_mb_strlen_8bit_name"); + emitter.instruction("mov rcx, 4"); // 8bit literal length + emitter.instruction("call __rt_strcasecmp"); // compare the explicit encoding with 8bit emitter.instruction("test eax, eax"); // did the encoding match 8bit? emitter.instruction("jz __rt_mb_strlen_use_byte_length_x86"); // 8bit counts every byte as one character emitter.instruction("lea rdi, [rbp - 160]"); // reload the copied encoding name for the binary alias - abi::emit_symbol_address(emitter, "rsi", "_mb_strlen_binary_name"); - emitter.instruction("call strcasecmp"); // compare the explicit encoding with binary + emitter.instruction("mov rsi, QWORD PTR [rbp - 64]"); // reload the explicit encoding-name length + abi::emit_symbol_address(emitter, "rdx", "_mb_strlen_binary_name"); + emitter.instruction("mov rcx, 6"); // binary literal length + emitter.instruction("call __rt_strcasecmp"); // compare the explicit encoding with binary emitter.instruction("test eax, eax"); // did the encoding match binary? emitter.instruction("jz __rt_mb_strlen_use_byte_length_x86"); // binary is PHP's alias for 8bit emitter.instruction("lea rdi, [rbp - 160]"); // reload the copied encoding name for the 7bit encoding - abi::emit_symbol_address(emitter, "rsi", "_mb_strlen_7bit_name"); - emitter.instruction("call strcasecmp"); // compare the explicit encoding with 7bit + emitter.instruction("mov rsi, QWORD PTR [rbp - 64]"); // reload the explicit encoding-name length + abi::emit_symbol_address(emitter, "rdx", "_mb_strlen_7bit_name"); + emitter.instruction("mov rcx, 4"); // 7bit literal length + emitter.instruction("call __rt_strcasecmp"); // compare the explicit encoding with 7bit emitter.instruction("test eax, eax"); // did the encoding match 7bit? emitter.instruction("jz __rt_mb_strlen_use_byte_length_x86"); // 7bit preserves PHP's one-character-per-byte count // -- open a decoder from the requested encoding to fixed-width UTF-32LE -- abi::emit_symbol_address(emitter, "rdi", "_mb_strlen_utf32le_name"); emitter.instruction("lea rsi, [rbp - 160]"); // iconv source encoding is the copied explicit name - emitter.instruction("call iconv_open"); // create the encoding-to-UTF-32LE conversion descriptor + emitter.emit_call_c("iconv_open"); // create the encoding-to-UTF-32LE conversion descriptor through the target ABI emitter.instruction("cmp rax, -1"); // did iconv_open return the failure sentinel? emitter.instruction("je __rt_mb_strlen_unknown_encoding_framed_x86"); // unknown encoding names raise PHP's ValueError emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // preserve the iconv descriptor across conversion iterations @@ -413,7 +424,7 @@ fn emit_mb_strlen_x86_64(emitter: &mut Emitter) { emitter.instruction("lea rdx, [rbp - 16]"); // iconv argument 2 is `&input_bytes_left` emitter.instruction("lea rcx, [rbp - 40]"); // iconv argument 3 is `&output_ptr` emitter.instruction("lea r8, [rbp - 48]"); // iconv argument 4 is `&output_bytes_left` - emitter.instruction("call iconv"); // decode as many complete characters as fit in the output scratch + emitter.emit_call_c("iconv"); // decode through the target ABI shim when required emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // preserve iconv's status while accounting for produced code points emitter.instruction("mov r10, 16"); // reload the fixed output scratch capacity emitter.instruction("sub r10, QWORD PTR [rbp - 48]"); // compute the number of UTF-32LE bytes produced @@ -436,7 +447,7 @@ fn emit_mb_strlen_x86_64(emitter: &mut Emitter) { emitter.instruction("xor rdx, rdx"); // no input byte count participates in the reset emitter.instruction("xor rcx, rcx"); // no output pointer participates in the reset emitter.instruction("xor r8, r8"); // no output byte count participates in the reset - emitter.instruction("call iconv"); // reset stateful decoders after substituting one malformed byte + emitter.emit_call_c("iconv"); // reset stateful decoders through the target ABI shim emitter.instruction("jmp __rt_mb_strlen_iconv_loop_x86"); // continue decoding after the malformed byte emitter.label("__rt_mb_strlen_iconv_incomplete_x86"); @@ -444,7 +455,7 @@ fn emit_mb_strlen_x86_64(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [rbp - 16], 0"); // mark the truncated suffix as fully handled emitter.label("__rt_mb_strlen_iconv_done_x86"); emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // iconv_close argument is the active conversion descriptor - emitter.instruction("call iconv_close"); // release the conversion descriptor before returning + emitter.emit_call_c("iconv_close"); // release the descriptor through the target ABI shim emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // return the accumulated character count emitter.instruction("leave"); // release the iconv helper frame and restore rbp emitter.instruction("ret"); // return the encoding-aware character count @@ -462,7 +473,7 @@ fn emit_mb_strlen_x86_64(emitter: &mut Emitter) { emitter.label("__rt_mb_strlen_unknown_encoding_framed_x86"); emitter.instruction("leave"); // release the explicit-encoding helper frame before unwinding - emitter.label("__rt_mb_strlen_unknown_encoding_x86"); + emitter.label_global("__rt_mb_strlen_unknown_encoding_x86"); value_error::emit_throw_value_error_x86_64( emitter, "_mb_strlen_unknown_encoding_msg", diff --git a/src/codegen_support/runtime/strings/mod.rs b/src/codegen_support/runtime/strings/mod.rs index 303f63207d..01dc2e6f9e 100644 --- a/src/codegen_support/runtime/strings/mod.rs +++ b/src/codegen_support/runtime/strings/mod.rs @@ -16,6 +16,7 @@ mod str_loose_eq; mod str_to_number; mod str_to_int; mod number_format; +mod php_round; mod atoi; mod grapheme_strrev; mod strcopy; @@ -58,6 +59,7 @@ mod html_entity_decode; mod urlencode; mod urldecode; mod rawurlencode; +mod shell_escape; mod base64_encode; mod base64_decode; mod sprintf; @@ -86,6 +88,8 @@ pub use concat::emit_concat; /// Emit string concatenation helper. pub use ftoa::emit_ftoa; /// Emit float-to-string conversion helper. +pub use ftoa::emit_var_dump_ftoa; +/// Emit the var_dump-precision (serialize_precision=-1, Inf/NaN-safe) float-to-string helper. pub use str_eq::emit_str_eq; /// Emit case-sensitive string equality check. pub use str_loose_eq::emit_str_loose_eq; @@ -98,6 +102,8 @@ pub use str_to_int::emit_str_to_int; /// Emit PHP string-to-integer cast helper. pub use number_format::emit_number_format; /// Emit number formatting helper. +pub use php_round::emit_php_round; +/// Emit the PHP-parity `_php_math_round` (HALF_UP) helper shared by `round()` and `number_format()`. pub use atoi::emit_atoi; /// Emit ASCII-to-integer conversion. pub use strcopy::emit_strcopy; @@ -182,6 +188,8 @@ pub use urldecode::emit_urldecode; /// Emit URL decoding helper. pub use rawurlencode::emit_rawurlencode; /// Emit raw URL encoding helper. +pub use shell_escape::emit_shell_escapes; +/// Emit PHP shell argument and command escaping helpers. pub use base64_encode::emit_base64_encode; /// Emit Base64 encoding helper. pub use base64_decode::emit_base64_decode; diff --git a/src/codegen_support/runtime/strings/number_format.rs b/src/codegen_support/runtime/strings/number_format.rs index 6bfb35048a..b51db06446 100644 --- a/src/codegen_support/runtime/strings/number_format.rs +++ b/src/codegen_support/runtime/strings/number_format.rs @@ -9,7 +9,7 @@ //! - String helpers use PHP pointer/length pairs and target ABI return registers; heap-backed results must remain refcount-compatible. use crate::codegen_support::emit::Emitter; -use crate::codegen_support::platform::Arch; +use crate::codegen_support::platform::{Arch, Platform}; /// Emits the `__rt_number_format` runtime helper. /// @@ -21,88 +21,122 @@ use crate::codegen_support::platform::Arch; /// Input registers (x86_64 SysV): `xmm0` = number, `rdi` = decimals, `rsi` = dec_point, `rdx` = thousands_sep /// Output registers (x86_64 SysV): `rax` = string pointer, `rdx` = string length /// -/// Stack frame layout (ARM64, 128 bytes): -/// `[sp+0..47]` snprintf buffer (48 bytes) -/// `[sp+64..68]` format string `"%.Nf\0"` -/// `[sp+72]` result start ptr -/// `[sp+80]` raw snprintf length -/// `[sp+88]` number (double) -/// `[sp+96]` decimals -/// `[sp+100]` dec_point char -/// `[sp+104]` thousands_sep char -/// `[sp+112]` saved x29, x30 +/// Stack frame layout (ARM64, 1152 bytes): +/// `[sp+0..1023]` snprintf buffer (1024 bytes) +/// `[sp+1032]` is_negative flag (PHP-parity pre-rounding strips the sign before +/// rounding; re-applied manually once the rounded value is known) +/// `[sp+1024..1028]` format string `"%.*f\0"` +/// `[sp+1040]` result start ptr +/// `[sp+1048]` raw snprintf length +/// `[sp+1056]` number (double) — becomes the `__rt_php_round`-rounded, non-negative value +/// `[sp+1064]` decimals — clamped to the safe supported range `0..=512` +/// `[sp+1068]` dec_point char +/// `[sp+1069]` thousands_sep char +/// `[sp+1136]` saved x29, x30 pub fn emit_number_format(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_number_format_linux_x86_64(emitter); return; } - // Stack frame layout (128 bytes): - // [sp+0..47] snprintf buffer (48 bytes) - // [sp+64..68] format string "%.Nf\0" - // [sp+72] result start ptr - // [sp+80] raw_len - // [sp+88] number (d0) - // [sp+96] decimals - // [sp+100] dec_point char - // [sp+104] thousands_sep char - // [sp+112] saved x29, x30 + // Stack frame layout (1152 bytes): + // [sp+0..1023] snprintf buffer (1024 bytes) + // [sp+1024..1028] format string "%.*f\0" + // [sp+1032] is_negative + // [sp+1040] result start ptr + // [sp+1048] raw_len + // [sp+1056] number (d0) + // [sp+1064] decimals + // [sp+1068] dec_point char + // [sp+1069] thousands_sep char + // [sp+1136] saved x29, x30 emitter.blank(); emitter.comment("--- runtime: number_format ---"); emitter.label_global("__rt_number_format"); - // -- set up stack frame (128 bytes) -- - emitter.instruction("sub sp, sp, #128"); // allocate 128 bytes on the stack - emitter.instruction("stp x29, x30, [sp, #112]"); // save frame pointer and return address - emitter.instruction("add x29, sp, #112"); // establish new frame pointer + // -- set up stack frame (1152 bytes) -- + emitter.instruction("sub sp, sp, #1152"); // allocate the bounded fixed-point formatting scratch frame + emitter.instruction("str x29, [sp, #1136]"); // save the frame pointer beyond the paired-store immediate range + emitter.instruction("str x30, [sp, #1144]"); // save the return address beside the frame pointer + emitter.instruction("add x29, sp, #1136"); // establish new frame pointer // -- save input arguments -- - emitter.instruction("str x1, [sp, #96]"); // save decimals count - emitter.instruction("str d0, [sp, #88]"); // save the floating-point number - emitter.instruction("str x2, [sp, #100]"); // save decimal point character - emitter.instruction("str x3, [sp, #104]"); // save thousands separator character + emitter.instruction("str w1, [sp, #1064]"); // save the signed decimals count consumed by PHP rounding + emitter.instruction("str d0, [sp, #1056]"); // save the floating-point number + emitter.instruction("strb w2, [sp, #1068]"); // save the decimal point character + emitter.instruction("strb w3, [sp, #1069]"); // save the thousands separator character - // -- build format string "%.Nf" at [sp+64] -- + // -- PHP-parity pre-rounding: strip the sign, round via __rt_php_round -- + // -- (php-src's _php_math_round, HALF_UP mode), then re-clamp decimals -- + emitter.instruction("fcmp d0, #0.0"); // compare the number against zero + emitter.instruction("cset x9, mi"); // is_negative = (number < 0.0) + emitter.instruction("str x9, [sp, #1032]"); // save the is_negative flag + emitter.instruction("fabs d0, d0"); // number = |number| + emitter.instruction("ldr w1, [sp, #1064]"); // places = decimals (may be negative) + emitter.instruction("sxtw x1, w1"); // sign-extend to the 64-bit places argument __rt_php_round expects + crate::codegen_support::abi::emit_call_label(emitter, "__rt_php_round"); // apply PHP's HALF_UP pre-rounding + emitter.instruction("str d0, [sp, #1056]"); // save the rounded, non-negative number + + emitter.instruction("ldr w9, [sp, #1064]"); // reload the requested decimals count + emitter.instruction("sxtw x9, w9"); // sign-extend for the comparison below + emitter.instruction("cmp x9, #0"); // test whether the requested decimal count is negative + emitter.instruction("csel x9, x9, xzr, ge"); // dec = max(0, dec), matching php-src + emitter.instruction("mov x10, #512"); // cap precision so every finite double fits the 1024-byte raw buffer + emitter.instruction("cmp x9, x10"); // compare the non-negative precision with the safe formatting bound + emitter.instruction("csel x9, x9, x10, le"); // precision = min(precision, 512) + emitter.instruction("str w9, [sp, #1064]"); // publish the bounded decimals count + + emitter.instruction("fcmp d0, #0.0"); // did rounding produce exactly zero? + emitter.instruction("b.ne __rt_nf_sign_kept"); // non-zero: keep whatever sign was recorded + emitter.instruction("str xzr, [sp, #1032]"); // PHP drops the minus sign for a rounded-to-zero result + emitter.label("__rt_nf_sign_kept"); + + // -- build the multi-digit precision format string "%.*f" at [sp+1024] -- emitter.instruction("mov w9, #37"); // ASCII '%' - emitter.instruction("strb w9, [sp, #64]"); // write '%' to format string + emitter.instruction("strb w9, [sp, #1024]"); // write '%' to format string emitter.instruction("mov w9, #46"); // ASCII '.' - emitter.instruction("strb w9, [sp, #65]"); // write '.' to format string - emitter.instruction("ldr x9, [sp, #96]"); // load decimals count - emitter.instruction("add w9, w9, #48"); // convert to ASCII digit ('0' + N) - emitter.instruction("strb w9, [sp, #66]"); // write decimal count digit + emitter.instruction("strb w9, [sp, #1025]"); // write '.' to format string + emitter.instruction("mov w9, #42"); // ASCII '*' + emitter.instruction("strb w9, [sp, #1026]"); // request precision from the next variadic integer argument emitter.instruction("mov w9, #102"); // ASCII 'f' - emitter.instruction("strb w9, [sp, #67]"); // write 'f' format specifier - emitter.instruction("strb wzr, [sp, #68]"); // null-terminate the format string + emitter.instruction("strb w9, [sp, #1027]"); // write 'f' format specifier + emitter.instruction("strb wzr, [sp, #1028]"); // null-terminate the format string - // -- call snprintf(buf, 48, fmt, d0) -- + // -- call snprintf(buf, 1024, fmt, precision, d0) -- emitter.instruction("add x0, sp, #0"); // x0 = output buffer at start of stack frame - emitter.instruction("mov x1, #48"); // buffer size = 48 bytes - emitter.instruction("add x2, sp, #64"); // x2 = format string pointer - emitter.instruction("ldr d0, [sp, #88]"); // reload the float value - emitter.instruction("str d0, [sp, #-16]!"); // push double for variadic ABI, adjust sp - emitter.bl_c("snprintf"); // call snprintf; returns char count in x0 - emitter.instruction("add sp, sp, #16"); // pop the variadic argument from stack - emitter.instruction("str x0, [sp, #80]"); // save raw string length + emitter.instruction("mov x1, #1024"); // buffer size = 1024 bytes + emitter.instruction("add x2, sp, #1024"); // x2 = format string pointer + emitter.instruction("ldr w3, [sp, #1064]"); // precision = bounded decimal count + emitter.instruction("ldr d0, [sp, #1056]"); // reload the float value + if emitter.platform == Platform::MacOS { + emitter.instruction("sub sp, sp, #16"); // reserve Apple's stack-only unnamed variadic argument area + emitter.instruction("str x3, [sp]"); // Apple vararg 1: precision integer + emitter.instruction("str d0, [sp, #8]"); // Apple vararg 2: formatted double + emitter.bl_c("snprintf"); // call snprintf; returns char count in x0 + emitter.instruction("add sp, sp, #16"); // release Apple's unnamed variadic argument area + } else { + emitter.bl_c("snprintf"); // AAPCS64 passes precision in x3 and the double in d0 + } + emitter.instruction("str x0, [sp, #1048]"); // save raw string length // -- set up concat_buf destination -- crate::codegen_support::abi::emit_symbol_address(emitter, "x6", "_concat_off"); emitter.instruction("ldr x8, [x6]"); // load current concat_buf write offset crate::codegen_support::abi::emit_symbol_address(emitter, "x7", "_concat_buf"); emitter.instruction("add x10, x7, x8"); // compute destination pointer - emitter.instruction("str x10, [sp, #72]"); // save result start pointer + emitter.instruction("str x10, [sp, #1040]"); // save result start pointer // -- scan raw string to find integer part length -- emitter.instruction("add x11, sp, #0"); // x11 = source ptr (snprintf output) - emitter.instruction("ldr x12, [sp, #80]"); // x12 = raw string length + emitter.instruction("ldr x12, [sp, #1048]"); // x12 = raw string length emitter.instruction("mov x13, #0"); // x13 = integer part digit count - // -- handle leading minus sign -- - emitter.instruction("ldrb w14, [x11]"); // load first character - emitter.instruction("cmp w14, #45"); // check if it's '-' (minus sign) - emitter.instruction("b.ne __rt_nf_count"); // skip if not negative - emitter.instruction("strb w14, [x10], #1"); // copy '-' to output, advance dest - emitter.instruction("add x11, x11, #1"); // advance source past '-' - emitter.instruction("sub x12, x12, #1"); // decrement remaining length + // -- emit the sign manually: the rounded number passed to snprintf is -- + // -- always non-negative now, so the raw output never carries one -- + emitter.instruction("ldr x14, [sp, #1032]"); // reload is_negative + emitter.instruction("cbz x14, __rt_nf_count"); // skip when the rounded result is non-negative + emitter.instruction("mov w15, #45"); // ASCII '-' + emitter.instruction("strb w15, [x10], #1"); // emit the sign, advance the dest cursor // -- count integer digits (before decimal point) -- emitter.label("__rt_nf_count"); @@ -136,7 +170,7 @@ pub fn emit_number_format(emitter: &mut Emitter) { emitter.instruction("cbz x16, __rt_nf_no_sep"); // skip separator before first digit emitter.instruction("cmp x14, #0"); // check if current group is exhausted emitter.instruction("b.ne __rt_nf_no_sep"); // group not done, no separator yet - emitter.instruction("ldr x9, [sp, #104]"); // load thousands separator char + emitter.instruction("ldrb w9, [sp, #1069]"); // load thousands separator char emitter.instruction("cbz x9, __rt_nf_no_sep_reset"); // skip if separator is 0 (none) emitter.instruction("strb w9, [x10], #1"); // write separator to output, advance dest emitter.label("__rt_nf_no_sep_reset"); @@ -157,7 +191,7 @@ pub fn emit_number_format(emitter: &mut Emitter) { emitter.instruction("ldrb w9, [x15], #1"); // load next decimal char, advance source emitter.instruction("cmp w9, #46"); // check if it's '.' (snprintf decimal point) emitter.instruction("b.ne __rt_nf_dec_store"); // if not '.', store as-is - emitter.instruction("ldr x9, [sp, #100]"); // replace with custom decimal point char + emitter.instruction("ldrb w9, [sp, #1068]"); // replace with custom decimal point char emitter.label("__rt_nf_dec_store"); emitter.instruction("strb w9, [x10], #1"); // write char to output, advance dest emitter.instruction("sub x12, x12, #1"); // decrement remaining chars @@ -165,19 +199,20 @@ pub fn emit_number_format(emitter: &mut Emitter) { // -- finalize: compute length and update concat_off -- emitter.label("__rt_nf_done"); - emitter.instruction("ldr x1, [sp, #72]"); // load result start pointer + emitter.instruction("ldr x1, [sp, #1040]"); // load result start pointer emitter.instruction("sub x2, x10, x1"); // result length = dest_end - dest_start emitter.instruction("ldr x8, [x6]"); // load current concat_off emitter.instruction("add x8, x8, x2"); // advance offset by result length emitter.instruction("str x8, [x6]"); // store updated concat_off // -- restore frame and return -- - emitter.instruction("ldp x29, x30, [sp, #112]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #128"); // deallocate stack frame + emitter.instruction("ldr x29, [sp, #1136]"); // restore the saved frame pointer + emitter.instruction("ldr x30, [sp, #1144]"); // restore the saved return address + emitter.instruction("add sp, sp, #1152"); // deallocate stack frame emitter.instruction("ret"); // return to caller } -/// ARM64 implementation of the `__rt_number_format` runtime helper. +/// x86_64 implementation of the `__rt_number_format` runtime helper. fn emit_number_format_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: number_format ---"); @@ -188,37 +223,71 @@ fn emit_number_format_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("push rbx"); // preserve the concat-buffer destination cursor across the local formatting and copy loops emitter.instruction("push r12"); // preserve the concat-buffer start pointer for the final x86_64 string return pair emitter.instruction("push r13"); // preserve the concat-offset symbol address across the local formatting and copy loops - emitter.instruction("sub rsp, 104"); // reserve local storage; bumped 96→104 so the four 8-byte saves above + this sub leave rsp 0-mod-16 before the SysV snprintf call below + emitter.instruction("sub rsp, 1080"); // reserve a 1024-byte raw buffer plus locals while keeping rsp 16-byte aligned for snprintf emitter.instruction("mov QWORD PTR [rbp - 56], rdi"); // preserve the requested decimal count across the intermediate formatting and copy loops emitter.instruction("mov QWORD PTR [rbp - 48], rsi"); // preserve the decimal-separator byte across the intermediate formatting and copy loops emitter.instruction("mov QWORD PTR [rbp - 40], rdx"); // preserve the thousands-separator byte across the intermediate formatting and copy loops - emitter.instruction("mov BYTE PTR [rbp - 72], 37"); // seed the mini format string with the leading '%' introducer - emitter.instruction("mov BYTE PTR [rbp - 71], 46"); // append the '.' precision introducer to the mini format string - emitter.instruction("mov r8, QWORD PTR [rbp - 56]"); // reload the requested decimal count before converting it into the single supported ASCII precision digit - emitter.instruction("add r8b, 48"); // convert the requested decimal count into its single-digit ASCII representation for the mini format string - emitter.instruction("mov BYTE PTR [rbp - 70], r8b"); // append the ASCII precision digit to the mini format string - emitter.instruction("mov BYTE PTR [rbp - 69], 102"); // append the trailing 'f' format type so snprintf renders a fixed-point decimal string - emitter.instruction("mov BYTE PTR [rbp - 68], 0"); // null-terminate the mini format string before handing it to snprintf - emitter.instruction("lea rdi, [rbp - 120]"); // point snprintf at the fixed local raw-decimal buffer that will be post-processed for thousands separators - emitter.instruction("mov esi, 48"); // bound the raw-decimal buffer to 48 bytes before the variadic snprintf call + + // -- PHP-parity pre-rounding: strip the sign, round via __rt_php_round -- + // -- (php-src's _php_math_round, HALF_UP mode), then re-clamp decimals -- + emitter.instruction("xorpd xmm1, xmm1"); // materialize a canonical 0.0 comparison operand + emitter.instruction("ucomisd xmm0, xmm1"); // compare the number against zero + emitter.instruction("setb al"); // is_negative = (number < 0.0) + emitter.instruction("movzx eax, al"); // widen the boolean byte + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the is_negative flag + emitter.instruction("movq r10, xmm0"); // copy the input double bits into an integer register + emitter.instruction("mov r11, 0x7fffffffffffffff"); // fabs mask + emitter.instruction("and r10, r11"); // clear the sign bit to compute the absolute value + emitter.instruction("movq xmm0, r10"); // number = |number| + emitter.instruction("mov rdi, QWORD PTR [rbp - 56]"); // places = decimals (may be negative) + crate::codegen_support::abi::emit_call_label(emitter, "__rt_php_round"); // apply PHP's HALF_UP pre-rounding + + emitter.instruction("mov r9, QWORD PTR [rbp - 56]"); // reload decimals + emitter.instruction("xor r10, r10"); // materialize zero for clamping negative precision + emitter.instruction("cmp r9, 0"); // test whether the requested decimal count is negative + emitter.instruction("cmovl r9, r10"); // dec = max(0, dec), matching php-src + emitter.instruction("mov r10, 512"); // cap precision so every finite double fits the 1024-byte raw buffer + emitter.instruction("cmp r9, r10"); // compare the non-negative precision with the safe formatting bound + emitter.instruction("cmovg r9, r10"); // precision = min(precision, 512) + emitter.instruction("mov QWORD PTR [rbp - 56], r9"); // publish the clamped decimals count + + emitter.instruction("xorpd xmm1, xmm1"); // materialize zero for the rounded-result sign check + emitter.instruction("ucomisd xmm0, xmm1"); // did rounding produce exactly zero? + emitter.instruction("jne __rt_nf_sign_kept_linux_x86_64"); // non-zero: keep whatever sign was recorded + emitter.instruction("mov QWORD PTR [rbp - 8], 0"); // PHP drops the minus sign for a rounded-to-zero result + emitter.label("__rt_nf_sign_kept_linux_x86_64"); + + emitter.instruction("mov BYTE PTR [rbp - 72], 37"); // seed the format string with the leading '%' introducer + emitter.instruction("mov BYTE PTR [rbp - 71], 46"); // append the '.' precision introducer + emitter.instruction("mov BYTE PTR [rbp - 70], 42"); // request precision from the next variadic integer argument + emitter.instruction("mov BYTE PTR [rbp - 69], 102"); // append the trailing 'f' fixed-point format type + emitter.instruction("mov BYTE PTR [rbp - 68], 0"); // null-terminate the "%.*f" format string + emitter.instruction("lea rdi, [rbp - 1104]"); // point snprintf at the bounded local raw-decimal buffer + emitter.instruction("mov esi, 1024"); // bound the raw-decimal buffer to 1024 bytes emitter.instruction("lea rdx, [rbp - 72]"); // pass the mini format string to snprintf as the fixed-point format pointer - emitter.instruction("mov eax, 1"); // advertise one live SIMD variadic register because the formatted number is passed in xmm0 on SysV x86_64 - emitter.bl_c("snprintf"); // render the raw fixed-point decimal string into the local snprintf buffer + emitter.instruction("mov rcx, QWORD PTR [rbp - 56]"); // pass the bounded precision as the first variadic argument + if emitter.platform == Platform::Windows { + emitter.instruction("call __rt_sys_snprintf"); // route precision arg4 and double arg5 through the matching MSx64 variadic shim + } else { + emitter.instruction("mov eax, 1"); // advertise one live SIMD variadic register because the formatted number is passed in xmm0 on SysV x86_64 + emitter.emit_call_c("snprintf"); // render the raw fixed-point decimal string into the local snprintf buffer + } emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // preserve the raw snprintf byte count before the thousands-separator pass consumes caller-saved registers crate::codegen_support::abi::emit_symbol_address(emitter, "r13", "_concat_off"); emitter.instruction("mov r8, QWORD PTR [r13]"); // load the current concat-buffer write cursor before appending the formatted output crate::codegen_support::abi::emit_symbol_address(emitter, "r9", "_concat_buf"); emitter.instruction("lea rbx, [r9 + r8]"); // compute the concat-buffer destination cursor where the formatted output will begin emitter.instruction("mov r12, rbx"); // preserve the concat-buffer start pointer for the final x86_64 string return pair - emitter.instruction("lea r10, [rbp - 120]"); // point at the raw snprintf output buffer before scanning for a leading minus sign and decimal point + emitter.instruction("lea r10, [rbp - 1104]"); // point at the raw snprintf output buffer before scanning for the decimal point emitter.instruction("mov rcx, QWORD PTR [rbp - 64]"); // reload the raw snprintf byte count before splitting the integer and decimal parts - emitter.instruction("movzx eax, BYTE PTR [r10]"); // peek at the first raw formatted byte to detect a leading minus sign - emitter.instruction("cmp al, 45"); // is the first raw formatted byte the leading '-' sign? - emitter.instruction("jne __rt_nf_count_linux_x86_64"); // skip the sign-copy fast path when the formatted number is non-negative - emitter.instruction("mov BYTE PTR [rbx], al"); // copy the leading minus sign into the concat buffer before processing the remaining digits - emitter.instruction("add rbx, 1"); // advance the concat-buffer destination cursor after copying the leading minus sign - emitter.instruction("add r10, 1"); // advance the raw formatted cursor past the copied leading minus sign - emitter.instruction("sub rcx, 1"); // decrement the remaining raw formatted byte count after removing the leading minus sign + + // -- emit the sign manually: the rounded number passed to snprintf is -- + // -- always non-negative now, so the raw output never carries one -- + emitter.instruction("mov r9, QWORD PTR [rbp - 8]"); // reload is_negative + emitter.instruction("test r9, r9"); // test whether the original number carried a negative sign + emitter.instruction("jz __rt_nf_count_linux_x86_64"); // skip when the rounded result is non-negative + emitter.instruction("mov BYTE PTR [rbx], 45"); // emit '-' + emitter.instruction("add rbx, 1"); // advance the concat-buffer destination cursor after emitting the sign emitter.label("__rt_nf_count_linux_x86_64"); emitter.instruction("mov r11, r10"); // preserve the start of the integer digit run before scanning forward to the decimal point @@ -291,10 +360,45 @@ fn emit_number_format_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov r8, QWORD PTR [r13]"); // reload the old concat-buffer write cursor before publishing the formatted-string append emitter.instruction("add r8, rdx"); // advance the concat-buffer write cursor by the emitted formatted-string length emitter.instruction("mov QWORD PTR [r13], r8"); // publish the updated concat-buffer write cursor after appending the formatted number - emitter.instruction("add rsp, 104"); // release the local raw-buffer and mini-format scratch space before restoring callee-saved registers + emitter.instruction("add rsp, 1080"); // release the local raw-buffer and mini-format scratch space before restoring callee-saved registers emitter.instruction("pop r13"); // restore the saved concat-offset symbol register after the x86_64 number_format() helper finishes emitter.instruction("pop r12"); // restore the saved concat-buffer start register after the x86_64 number_format() helper finishes emitter.instruction("pop rbx"); // restore the saved concat-buffer destination cursor register after the x86_64 number_format() helper finishes emitter.instruction("pop rbp"); // restore the caller frame pointer before returning the x86_64 formatted string pair emitter.instruction("ret"); // return the formatted string pointer and length in the standard x86_64 string result registers } + +#[cfg(test)] +mod tests { + use crate::codegen_support::platform::Target; + + use super::*; + + /// Verifies multi-digit precision uses the MSx64 shim matching + /// `snprintf(buf, size, "%.*f", precision, double)`: precision is positional + /// argument four and the double is positional argument five on the stack. + #[test] + fn test_number_format_windows_x86_64_routes_through_precision_snprintf_shim() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_number_format(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains("mov BYTE PTR [rbp - 70], 42\n")); + assert!(asm.contains("mov rcx, QWORD PTR [rbp - 56]\n")); + assert!(asm.contains("call __rt_sys_snprintf\n")); + assert!(!asm.contains("call __rt_sys_snprintf_double\n")); + } + + /// Companion non-Windows control: the SysV variadic staging (`mov eax, 1` + /// then a bare `call snprintf`) must stay unchanged on Linux/macOS x86_64. + #[test] + fn test_number_format_linux_x86_64_still_calls_bare_snprintf() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_number_format(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains("mov eax, 1\n")); + assert!(asm.contains("call snprintf\n")); + assert!(!asm.contains("__rt_sys_snprintf_double")); + } +} diff --git a/src/codegen_support/runtime/strings/php_round.rs b/src/codegen_support/runtime/strings/php_round.rs new file mode 100644 index 0000000000..005f411dd8 --- /dev/null +++ b/src/codegen_support/runtime/strings/php_round.rs @@ -0,0 +1,457 @@ +//! Purpose: +//! Emits the `__rt_php_round` runtime helper: a faithful port of php-src's +//! `_php_math_round` (`ext/standard/math.c`, PHP-8.4 branch) restricted to the +//! `PHP_ROUND_HALF_UP` mode, the only mode elephc's `round()` builtin and +//! `number_format()` ever request. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::strings`. +//! - `crate::codegen::lower_inst::builtins::math::lower_round()` (the `round()` builtin). +//! - `crate::codegen_support::runtime::strings::number_format::emit_number_format()` (PHP's +//! pre-rounding step before `"%.*F"` formatting). +//! +//! Key details: +//! - PHP's CRT-backed `%.Nf`/`round()` round half-to-even; PHP itself rounds half +//! *away* from zero, with a pre-correction step that compensates for the binary +//! representation error introduced by scaling by `10**places` (e.g. +//! `0.285 * 1e10 == 2849999999.9999995`, whose `floor()` is one short). This +//! helper reproduces both the correction and the half-away-from-zero edge-case +//! test bit-for-bit against php-src, using hardware `floor`/`ceil` (`frintm`/ +//! `frintp` on AArch64, `roundsd` modes 1/2 on x86_64) and a single `pow()` call +//! for the `10**abs(places)` exponent (no lookup-table fast path — correctness +//! over the table's micro-optimization, and libm's `pow` is correctly rounded +//! for exact integer powers in this range so the result is bit-identical). +//! - `copysign()` is inlined as sign-bit extraction + OR with the compile-time-known +//! magnitude bits of `0.5`/`1.0` (no libm call needed); `fabs()` uses the native +//! `fabs` instruction on AArch64 and the sign-mask-AND trick on x86_64, matching +//! `crate::codegen::lower_inst::builtins::math::emit_float_abs`. +//! - Intentionally omits php-src's `abs(places) >= 23` string-round-trip branch +//! (snprintf/strtod through a scratch buffer): that path only matters for +//! precision requests beyond a double's ~15-17 significant decimal digits, +//! which is already past the point where rounding is meaningful. Places in +//! that range fall through to the same simple division/multiplication step +//! used for `abs(places) < 23`, which is what php-src does through +//! `pow()`-backed `php_intpow10` beyond its 22-entry table anyway. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// Emits the `__rt_php_round` runtime helper (`_php_math_round`, `PHP_ROUND_HALF_UP`). +/// +/// # Input +/// - ARM64: `d0` = value, `x1` = places (signed 64-bit precision) +/// - x86_64: `xmm0` = value, `rdi` = places (signed 64-bit precision) +/// +/// # Output +/// - ARM64: `d0` = rounded value +/// - x86_64: `xmm0` = rounded value +/// +/// # Behavior +/// Non-finite and zero inputs are returned unchanged. Otherwise: `exponent = +/// pow(10.0, abs(places))`; a floor/ceil pre-rounding step (chosen by the sign +/// of `value`) is corrected by one ULP-of-precision when scaling it back +/// exactly reproduces the input (php-src's representation-error fix-up); values +/// whose corrected magnitude is `>= 1e16` are returned unchanged (beyond double +/// precision, matching php-src); otherwise the `HALF_UP` edge case rounds the +/// corrected integral part away from zero when `|value|` reaches the scaled +/// midpoint, and the result is scaled back to the requested precision. +pub fn emit_php_round(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_php_round_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: php_round (_php_math_round port, HALF_UP mode) ---"); + emitter.label_global("__rt_php_round"); + + // -- stack frame (128 bytes): value/places/exponent/tmp scratch + saved fp/lr -- + emitter.instruction("sub sp, sp, #128"); // allocate the php_round scratch frame + emitter.instruction("stp x29, x30, [sp, #112]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #112"); // establish new frame pointer + + emitter.instruction("str d0, [sp, #0]"); // save the original value + emitter.instruction("str x1, [sp, #8]"); // save places (pre-clamp) + + // -- non-finite check: exponent field == 0x7FF means Inf or NaN -- + emitter.instruction("fmov x9, d0"); // grab the raw bit pattern of the input double + emitter.instruction("lsr x10, x9, #52"); // shift the exponent field into the low bits + emitter.instruction("and x10, x10, #0x7ff"); // isolate the 11-bit exponent + emitter.instruction("cmp x10, #0x7ff"); // is the exponent all ones (Inf/NaN)? + emitter.instruction("b.eq __rt_php_round_done"); // non-finite: d0 already holds the unchanged input + + // -- zero check: _php_math_round returns +/-0.0 unchanged -- + emitter.instruction("fcmp d0, #0.0"); // compare the input against zero + emitter.instruction("b.eq __rt_php_round_done"); // zero: d0 already holds the unchanged input + + // -- clamp places to avoid abs() overflow at i64::MIN (mirrors PHP's INT_MIN+1 clamp) -- + emitter.instruction("mov x9, #1"); // build the i64::MIN sentinel via shift (avoids a 64-bit immediate load) + emitter.instruction("lsl x9, x9, #63"); // x9 = i64::MIN (0x8000000000000000) + emitter.instruction("ldr x10, [sp, #8]"); // reload places + emitter.instruction("cmp x10, x9"); // is places exactly i64::MIN? + emitter.instruction("b.ne __rt_php_round_no_clamp"); // skip the clamp for every other value + emitter.instruction("add x10, x10, #1"); // clamp i64::MIN to i64::MIN+1 so -places cannot overflow + emitter.label("__rt_php_round_no_clamp"); + emitter.instruction("str x10, [sp, #8]"); // store the clamped places + + // -- exponent = pow(10.0, (double)abs(places)) -- + emitter.instruction("cmp x10, #0"); // is places already non-negative? + emitter.instruction("b.ge __rt_php_round_abs_pos"); // non-negative places is its own magnitude + emitter.instruction("neg x11, x10"); // magnitude of a negative places + emitter.instruction("b __rt_php_round_abs_done"); // continue with the absolute-value bit pattern + emitter.label("__rt_php_round_abs_pos"); + emitter.instruction("mov x11, x10"); // copy the input bits before clearing their sign + emitter.label("__rt_php_round_abs_done"); + emitter.instruction("scvtf d1, x11"); // abs(places) as a double exponent for pow() + emitter.instruction("fmov d0, #10.0"); // base 10.0 for the power computation + emitter.emit_call_c("pow"); // exponent = 10.0 ** abs(places) + emitter.instruction("str d0, [sp, #16]"); // save the exponent for reuse across every branch below + + // -- scaled = places>0 ? value*exponent : value/exponent -- + emitter.instruction("ldr d0, [sp, #0]"); // reload the original value + emitter.instruction("ldr d2, [sp, #16]"); // reload the exponent + emitter.instruction("ldr x10, [sp, #8]"); // reload the clamped places + emitter.instruction("cmp x10, #0"); // choose multiplication or division from the place sign + emitter.instruction("b.le __rt_php_round_scale_div"); // non-positive places scale by division + emitter.instruction("fmul d3, d0, d2"); // scaled = value * exponent + emitter.instruction("b __rt_php_round_scale_done"); // skip the division scaling path + emitter.label("__rt_php_round_scale_div"); + emitter.instruction("fdiv d3, d0, d2"); // scaled = value / exponent + emitter.label("__rt_php_round_scale_done"); + + // -- tmp_value/tmp_value2: floor+1 for non-negative values, ceil-1 for negative -- + emitter.instruction("fcmp d0, #0.0"); // branch on the sign of the ORIGINAL value + emitter.instruction("b.lt __rt_php_round_neg_branch"); // use the negative-value HALF_UP branch below zero + emitter.instruction("frintm d4, d3"); // tmp_value = floor(scaled) + emitter.instruction("fmov d5, #1.0"); // materialize the positive correction unit + emitter.instruction("fadd d5, d4, d5"); // tmp_value2 = tmp_value + 1.0 + emitter.instruction("b __rt_php_round_tmp_done"); // skip construction of the negative correction unit + emitter.label("__rt_php_round_neg_branch"); + emitter.instruction("frintp d4, d3"); // tmp_value = ceil(scaled) + emitter.instruction("fmov d5, #1.0"); // materialize the correction unit before negation + emitter.instruction("fsub d5, d4, d5"); // tmp_value2 = tmp_value - 1.0 + emitter.label("__rt_php_round_tmp_done"); + emitter.instruction("str d4, [sp, #24]"); // save tmp_value + emitter.instruction("str d5, [sp, #32]"); // save tmp_value2 + + // -- representation-error correction: adopt tmp_value2 when it round-trips exactly -- + emitter.instruction("cmp x10, #0"); // select how the rounded integer is rescaled for checking + emitter.instruction("b.le __rt_php_round_check_mul"); // non-positive places restore scale by multiplication + emitter.instruction("fdiv d6, d5, d2"); // check = tmp_value2 / exponent + emitter.instruction("b __rt_php_round_check_done"); // skip the multiplication check path + emitter.label("__rt_php_round_check_mul"); + emitter.instruction("fmul d6, d5, d2"); // check = tmp_value2 * exponent + emitter.label("__rt_php_round_check_done"); + emitter.instruction("fcmp d6, d0"); // does the round-trip reproduce the original value exactly? + emitter.instruction("b.ne __rt_php_round_no_correction"); // bypass edge correction when scaling changed the value + emitter.instruction("str d5, [sp, #24]"); // correction applies: tmp_value = tmp_value2 + emitter.label("__rt_php_round_no_correction"); + + // -- values beyond double precision (fabs(tmp_value) >= 1e16) are returned unchanged -- + emitter.instruction("ldr d4, [sp, #24]"); // reload the (possibly corrected) tmp_value + emitter.instruction("fabs d6, d4"); // compare the rounded magnitude without its sign + emit_load_1e16_aarch64(emitter, "x11"); + emitter.instruction("fmov d7, x11"); // d7 = 1e16 + emitter.instruction("fcmp d6, d7"); // test the rounded magnitude against the correction edge + emitter.instruction("b.lt __rt_php_round_helper"); // < 1e16: continue to the HALF_UP edge-case test + emitter.instruction("ldr d0, [sp, #0]"); // >= 1e16: restore and return the original value unchanged + emitter.instruction("b __rt_php_round_done"); // return the corrected rounded value directly + + // -- HALF_UP edge case: round the integral part away from zero at the midpoint -- + emitter.label("__rt_php_round_helper"); + emitter.instruction("mov x9, #1"); // seed the positive HALF_UP correction integer + emitter.instruction("lsl x9, x9, #63"); // sign-bit mask + emitter.instruction("fmov x12, d4"); // bits of tmp_value + emitter.instruction("and x12, x12, x9"); // isolate the sign bit of tmp_value + emitter.instruction("movz x13, #0x3fe0, lsl #48"); // magnitude bits of 0.5 + emitter.instruction("orr x13, x12, x13"); // x13 = copysign(0.5, tmp_value) bits + emitter.instruction("fmov d5, x13"); // reinterpret the signed correction bits as a double + emitter.instruction("fadd d5, d4, d5"); // tmp_value + copysign(0.5, tmp_value) + emitter.instruction("ldr x10, [sp, #8]"); // reload the clamped places + emitter.instruction("ldr d2, [sp, #16]"); // reload the exponent + emitter.instruction("cmp x10, #0"); // select the place-dependent correction rescaling path + emitter.instruction("b.le __rt_php_round_edge_mul"); // non-positive places scale the correction by multiplication + emitter.instruction("fdiv d6, d5, d2"); // shrink the correction for positive decimal places + emitter.instruction("b __rt_php_round_edge_done"); // skip the multiplication rescaling path + emitter.label("__rt_php_round_edge_mul"); + emitter.instruction("fmul d6, d5, d2"); // enlarge the correction for non-positive places + emitter.label("__rt_php_round_edge_done"); + emitter.instruction("fabs d6, d6"); // edge_case + emitter.instruction("ldr d0, [sp, #0]"); // reload the original value + emitter.instruction("fabs d1, d0"); // value_abs + emitter.instruction("fcmp d1, d6"); // value_abs vs edge_case + emitter.instruction("b.lt __rt_php_round_no_round_up"); // value_abs < edge_case: keep tmp_value as-is + + emitter.instruction("mov x9, #1"); // seed the positive fallback correction integer + emitter.instruction("lsl x9, x9, #63"); // sign-bit mask + emitter.instruction("fmov x12, d4"); // bits of tmp_value + emitter.instruction("and x12, x12, x9"); // isolate the sign bit of tmp_value + emitter.instruction("movz x13, #0x3ff0, lsl #48"); // magnitude bits of 1.0 + emitter.instruction("orr x13, x12, x13"); // x13 = copysign(1.0, tmp_value) bits + emitter.instruction("fmov d5, x13"); // reinterpret the signed fallback correction as a double + emitter.instruction("fadd d4, d4, d5"); // round away from zero + emitter.label("__rt_php_round_no_round_up"); + + // -- final: scale tmp_value back to the requested precision -- + emitter.instruction("ldr x10, [sp, #8]"); // reload the requested decimal-place count + emitter.instruction("ldr d2, [sp, #16]"); // reload the decimal scaling factor + emitter.instruction("cmp x10, #0"); // select the final inverse-scaling operation + emitter.instruction("b.le __rt_php_round_final_mul"); // non-positive places restore scale by multiplication + emitter.instruction("fdiv d0, d4, d2"); // restore positive-place scaling by division + emitter.instruction("b __rt_php_round_done"); // skip the multiplication result path + emitter.label("__rt_php_round_final_mul"); + emitter.instruction("fmul d0, d4, d2"); // restore non-positive-place scaling by multiplication + + emitter.label("__rt_php_round_done"); + emitter.instruction("ldp x29, x30, [sp, #112]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #128"); // deallocate stack frame + emitter.instruction("ret"); // return the PHP-compatible rounded double +} + +/// Materializes the IEEE-754 bit pattern of `1e16` into `reg` via `movz`/`movk` +/// (the ARM64 `fmov` immediate encoding cannot represent it directly). +fn emit_load_1e16_aarch64(emitter: &mut Emitter, reg: &str) { + emitter.instruction(&format!("movz {}, #0x8000", reg)); // low 16 bits of 1e16 (0x4341C37937E08000) + emitter.instruction(&format!("movk {}, #0x37e0, lsl #16", reg)); // next 16 bits + emitter.instruction(&format!("movk {}, #0xc379, lsl #32", reg)); // next 16 bits + emitter.instruction(&format!("movk {}, #0x4341, lsl #48", reg)); // top 16 bits +} + +/// Emits the x86_64 variant of `__rt_php_round` (Linux, macOS, and windows-x86_64). +/// Mirrors [`emit_php_round`]'s AArch64 body exactly; see that function's doc +/// comment for the algorithm. Every `pow()` call routes through +/// [`Emitter::emit_call_c`] so windows-x86_64 reaches the registered +/// `__rt_sys_pow` msvcrt shim instead of a raw SysV-staged import call. +fn emit_php_round_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: php_round (_php_math_round port, HALF_UP mode) ---"); + emitter.label_global("__rt_php_round"); + + emitter.instruction("push rbp"); // save the caller frame pointer before using stack locals + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the php_round scratch slots + emitter.instruction("sub rsp, 64"); // reserve scratch (value/places/exponent/tmp_value/tmp_value2), 16-byte aligned + + emitter.instruction("movsd QWORD PTR [rbp - 8], xmm0"); // save the original value + emitter.instruction("mov QWORD PTR [rbp - 16], rdi"); // save places (pre-clamp) + + // -- non-finite check: exponent field == 0x7FF means Inf or NaN -- + emitter.instruction("movq r9, xmm0"); // grab the raw bit pattern of the input double + emitter.instruction("mov r10, r9"); // copy the bits for exponent extraction + emitter.instruction("shr r10, 52"); // shift the exponent field into the low bits + emitter.instruction("and r10, 0x7ff"); // isolate the 11-bit exponent + emitter.instruction("cmp r10, 0x7ff"); // is the exponent all ones (Inf/NaN)? + emitter.instruction("je __rt_php_round_done_x"); // non-finite: xmm0 already holds the unchanged input + + // -- zero check: _php_math_round returns +/-0.0 unchanged -- + emitter.instruction("xorpd xmm1, xmm1"); // materialize a canonical 0.0 comparison operand + emitter.instruction("ucomisd xmm0, xmm1"); // compare the input against zero + emitter.instruction("je __rt_php_round_done_x"); // zero: xmm0 already holds the unchanged input + + // -- clamp places to avoid abs() overflow at i64::MIN (mirrors PHP's INT_MIN+1 clamp) -- + emitter.instruction("mov r9, QWORD PTR [rbp - 16]"); // reload places + emitter.instruction("mov r10, 0x8000000000000000"); // i64::MIN sentinel + emitter.instruction("cmp r9, r10"); // is places exactly i64::MIN? + emitter.instruction("jne __rt_php_round_no_clamp_x"); // skip the clamp for every other value + emitter.instruction("add r9, 1"); // clamp i64::MIN to i64::MIN+1 so -places cannot overflow + emitter.label("__rt_php_round_no_clamp_x"); + emitter.instruction("mov QWORD PTR [rbp - 16], r9"); // store the clamped places + + // -- exponent = pow(10.0, (double)abs(places)) -- + emitter.instruction("cmp r9, 0"); // is places already non-negative? + emitter.instruction("jge __rt_php_round_abs_pos_x"); // non-negative places is its own magnitude + emitter.instruction("mov r10, r9"); // copy the input bits before clearing their sign + emitter.instruction("neg r10"); // magnitude of a negative places + emitter.instruction("jmp __rt_php_round_abs_done_x"); // continue with the absolute-value bit pattern + emitter.label("__rt_php_round_abs_pos_x"); + emitter.instruction("mov r10, r9"); // copy negative input bits before clearing their sign + emitter.label("__rt_php_round_abs_done_x"); + emitter.instruction("cvtsi2sd xmm1, r10"); // abs(places) as a double exponent for pow() + emitter.instruction("mov rax, 0x4024000000000000"); // IEEE-754 payload for 10.0 + emitter.instruction("movq xmm0, rax"); // base 10.0 for the power computation + emitter.emit_call_c("pow"); // exponent = 10.0 ** abs(places) + emitter.instruction("movsd QWORD PTR [rbp - 24], xmm0"); // save the exponent for reuse across every branch below + + // -- scaled = places>0 ? value*exponent : value/exponent -- + emitter.instruction("movsd xmm0, QWORD PTR [rbp - 8]"); // reload the original value + emitter.instruction("movsd xmm2, QWORD PTR [rbp - 24]"); // reload the exponent + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the clamped places + emitter.instruction("cmp r10, 0"); // choose multiplication or division from the place sign + emitter.instruction("jle __rt_php_round_scale_div_x"); // non-positive places scale by division + emitter.instruction("movsd xmm3, xmm0"); // copy the absolute input before multiplying its scale + emitter.instruction("mulsd xmm3, xmm2"); // scaled = value * exponent + emitter.instruction("jmp __rt_php_round_scale_done_x"); // skip the division scaling path + emitter.label("__rt_php_round_scale_div_x"); + emitter.instruction("movsd xmm3, xmm0"); // copy the absolute input before dividing its scale + emitter.instruction("divsd xmm3, xmm2"); // scaled = value / exponent + emitter.label("__rt_php_round_scale_done_x"); + + // -- tmp_value/tmp_value2: floor+1 for non-negative values, ceil-1 for negative -- + emitter.instruction("xorpd xmm4, xmm4"); // canonical 0.0 for the sign branch + emitter.instruction("ucomisd xmm0, xmm4"); // branch on the sign of the ORIGINAL value + emitter.instruction("jb __rt_php_round_neg_branch_x"); // use the negative-value HALF_UP branch below zero + emitter.instruction("roundsd xmm4, xmm3, 1"); // tmp_value = floor(scaled) + emitter.instruction("mov rax, 0x3ff0000000000000"); // IEEE-754 payload for 1.0 + emitter.instruction("movq xmm5, rax"); // materialize the positive correction unit + emitter.instruction("movsd xmm6, xmm4"); // copy the scaled positive magnitude for adjustment + emitter.instruction("addsd xmm6, xmm5"); // tmp_value2 = tmp_value + 1.0 + emitter.instruction("jmp __rt_php_round_tmp_done_x"); // skip construction of the negative correction unit + emitter.label("__rt_php_round_neg_branch_x"); + emitter.instruction("roundsd xmm4, xmm3, 2"); // tmp_value = ceil(scaled) + emitter.instruction("mov rax, 0x3ff0000000000000"); // IEEE-754 payload for 1.0 + emitter.instruction("movq xmm5, rax"); // materialize the negative correction unit + emitter.instruction("movsd xmm6, xmm4"); // copy the scaled negative magnitude for adjustment + emitter.instruction("subsd xmm6, xmm5"); // tmp_value2 = tmp_value - 1.0 + emitter.label("__rt_php_round_tmp_done_x"); + emitter.instruction("movsd QWORD PTR [rbp - 32], xmm4"); // save tmp_value + emitter.instruction("movsd QWORD PTR [rbp - 40], xmm6"); // save tmp_value2 + + // -- representation-error correction: adopt tmp_value2 when it round-trips exactly -- + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the clamped places + emitter.instruction("movsd xmm2, QWORD PTR [rbp - 24]"); // reload the exponent + emitter.instruction("cmp r10, 0"); // select how the rounded integer is rescaled for checking + emitter.instruction("jle __rt_php_round_check_mul_x"); // non-positive places restore scale by multiplication + emitter.instruction("movsd xmm7, xmm6"); // copy the rounded integer for division rescaling + emitter.instruction("divsd xmm7, xmm2"); // check = tmp_value2 / exponent + emitter.instruction("jmp __rt_php_round_check_done_x"); // skip the multiplication check path + emitter.label("__rt_php_round_check_mul_x"); + emitter.instruction("movsd xmm7, xmm6"); // copy the rounded integer for multiplication rescaling + emitter.instruction("mulsd xmm7, xmm2"); // check = tmp_value2 * exponent + emitter.label("__rt_php_round_check_done_x"); + emitter.instruction("movsd xmm0, QWORD PTR [rbp - 8]"); // reload the original value for the comparison + emitter.instruction("ucomisd xmm7, xmm0"); // does the round-trip reproduce the original value exactly? + emitter.instruction("jne __rt_php_round_no_correction_x"); // bypass edge correction when scaling changed the value + emitter.instruction("movsd QWORD PTR [rbp - 32], xmm6"); // correction applies: tmp_value = tmp_value2 + emitter.label("__rt_php_round_no_correction_x"); + + // -- values beyond double precision (fabs(tmp_value) >= 1e16) are returned unchanged -- + emitter.instruction("movsd xmm4, QWORD PTR [rbp - 32]"); // reload the (possibly corrected) tmp_value + emitter.instruction("movq r10, xmm4"); // extract rounded bits for absolute-value comparison + emitter.instruction("mov r11, 0x7fffffffffffffff"); // fabs mask + emitter.instruction("and r10, r11"); // clear the rounded value's sign bit + emitter.instruction("movq xmm6, r10"); // xmm6 = fabs(tmp_value) + emitter.instruction("mov rax, 0x4341c37937e08000"); // IEEE-754 payload for 1e16 + emitter.instruction("movq xmm7, rax"); // materialize the magnitude correction threshold + emitter.instruction("ucomisd xmm6, xmm7"); // compare the rounded magnitude with the threshold + emitter.instruction("jb __rt_php_round_helper_x"); // < 1e16: continue to the HALF_UP edge-case test + emitter.instruction("movsd xmm0, QWORD PTR [rbp - 8]"); // >= 1e16: restore and return the original value unchanged + emitter.instruction("jmp __rt_php_round_done_x"); // return the corrected rounded value directly + + // -- HALF_UP edge case: round the integral part away from zero at the midpoint -- + emitter.label("__rt_php_round_helper_x"); + emitter.instruction("movq r9, xmm4"); // bits of tmp_value + emitter.instruction("mov r10, 0x8000000000000000"); // sign-bit mask + emitter.instruction("and r9, r10"); // isolate the sign bit of tmp_value + emitter.instruction("mov r11, 0x3fe0000000000000"); // magnitude bits of 0.5 + emitter.instruction("or r9, r11"); // r9 = copysign(0.5, tmp_value) bits + emitter.instruction("movq xmm5, r9"); // materialize the signed edge correction unit + emitter.instruction("movsd xmm6, xmm4"); // copy the scaled magnitude for edge adjustment + emitter.instruction("addsd xmm6, xmm5"); // tmp_value + copysign(0.5, tmp_value) + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the clamped places + emitter.instruction("movsd xmm2, QWORD PTR [rbp - 24]"); // reload the exponent + emitter.instruction("cmp r10, 0"); // select the place-dependent correction rescaling path + emitter.instruction("jle __rt_php_round_edge_mul_x"); // non-positive places scale the correction by multiplication + emitter.instruction("divsd xmm6, xmm2"); // shrink the correction for positive decimal places + emitter.instruction("jmp __rt_php_round_edge_done_x"); // skip the multiplication rescaling path + emitter.label("__rt_php_round_edge_mul_x"); + emitter.instruction("mulsd xmm6, xmm2"); // enlarge the correction for non-positive places + emitter.label("__rt_php_round_edge_done_x"); + emitter.instruction("movq r10, xmm6"); // extract correction bits for an absolute-value comparison + emitter.instruction("mov r11, 0x7fffffffffffffff"); // fabs mask + emitter.instruction("and r10, r11"); // clear the correction value's sign bit + emitter.instruction("movq xmm6, r10"); // edge_case = fabs(...) + emitter.instruction("movsd xmm0, QWORD PTR [rbp - 8]"); // reload the original value + emitter.instruction("movq r10, xmm0"); // extract input bits for an absolute-value comparison + emitter.instruction("and r10, r11"); // clear the original input's sign bit + emitter.instruction("movq xmm1, r10"); // value_abs = fabs(value) + emitter.instruction("ucomisd xmm1, xmm6"); // value_abs vs edge_case + emitter.instruction("jb __rt_php_round_no_round_up_x"); // value_abs < edge_case: keep tmp_value as-is + + emitter.instruction("movq r9, xmm4"); // bits of tmp_value + emitter.instruction("mov r10, 0x8000000000000000"); // sign-bit mask + emitter.instruction("and r9, r10"); // isolate the sign bit of tmp_value + emitter.instruction("mov r11, 0x3ff0000000000000"); // magnitude bits of 1.0 + emitter.instruction("or r9, r11"); // r9 = copysign(1.0, tmp_value) bits + emitter.instruction("movq xmm5, r9"); // materialize the signed fallback correction unit + emitter.instruction("addsd xmm4, xmm5"); // round away from zero + emitter.label("__rt_php_round_no_round_up_x"); + + // -- final: scale tmp_value back to the requested precision -- + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the requested decimal-place count + emitter.instruction("movsd xmm2, QWORD PTR [rbp - 24]"); // reload the decimal scaling factor + emitter.instruction("cmp r10, 0"); // select the final inverse-scaling operation + emitter.instruction("jle __rt_php_round_final_mul_x"); // non-positive places restore scale by multiplication + emitter.instruction("movsd xmm0, xmm4"); // copy the rounded value for division rescaling + emitter.instruction("divsd xmm0, xmm2"); // restore positive-place scaling by division + emitter.instruction("jmp __rt_php_round_done_x"); // skip the multiplication result path + emitter.label("__rt_php_round_final_mul_x"); + emitter.instruction("movsd xmm0, xmm4"); // copy the rounded value for multiplication rescaling + emitter.instruction("mulsd xmm0, xmm2"); // restore non-positive-place scaling by multiplication + + emitter.label("__rt_php_round_done_x"); + emitter.instruction("add rsp, 64"); // release the local scratch area before returning + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the rounded value in xmm0 +} + +#[cfg(test)] +mod tests { + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::*; + + /// Verifies the shared label and the AArch64 non-finite/zero early-return + /// checks (mirroring `__rt_ftoa`'s proven exponent-field test) are present. + #[test] + fn test_emit_php_round_aarch64_early_returns() { + let mut emitter = Emitter::new(Target::new(Platform::MacOS, Arch::AArch64)); + emit_php_round(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains("__rt_php_round:\n")); + assert!(asm.contains("and x10, x10, #0x7ff\n")); + assert!(asm.contains("fcmp d0, #0.0\n")); + assert!(asm.contains("bl _pow\n")); + } + + /// Verifies the HALF_UP edge-case copysign bit tricks and the 1e16 overflow + /// guard are present on AArch64 (no libm `copysign`/`fabs` calls). + #[test] + fn test_emit_php_round_aarch64_uses_bit_tricks_not_libm_copysign() { + let mut emitter = Emitter::new(Target::new(Platform::MacOS, Arch::AArch64)); + emit_php_round(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains("movz x13, #0x3fe0, lsl #48\n")); + assert!(asm.contains("movz x13, #0x3ff0, lsl #48\n")); + assert!(!asm.contains("bl _copysign\n")); + assert!(!asm.contains("bl _fabs\n")); + } + + /// Verifies x86_64 routes `pow()` through `emit_call_c` (Windows-shim-safe) + /// and uses the `roundsd` floor/ceil immediates already proven by + /// `lower_floor`/`lower_ceil`. + #[test] + fn test_emit_php_round_x86_64_uses_roundsd_and_call_c_pow() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_php_round(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains("__rt_php_round:\n")); + assert!(asm.contains("roundsd xmm4, xmm3, 1\n")); + assert!(asm.contains("roundsd xmm4, xmm3, 2\n")); + assert!(asm.contains("call pow\n")); + } + + /// Regression guard: windows-x86_64 must route `pow()` through the + /// registered `__rt_sys_pow` shim, not a bare `call pow` (the Class-1 + /// SysV/MSx64 ABI bug this codebase has hit before). + #[test] + fn test_emit_php_round_windows_x86_64_routes_pow_through_shim() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_php_round(&mut emitter); + let asm = emitter.output(); + + assert!(!asm.contains("call pow\n")); + assert!(asm.contains("call __rt_sys_pow\n")); + } +} diff --git a/src/codegen_support/runtime/strings/shell_escape.rs b/src/codegen_support/runtime/strings/shell_escape.rs new file mode 100644 index 0000000000..aee907ab98 --- /dev/null +++ b/src/codegen_support/runtime/strings/shell_escape.rs @@ -0,0 +1,945 @@ +//! Purpose: +//! Emits PHP-compatible `escapeshellarg()` and `escapeshellcmd()` runtime helpers. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` through the string runtime module. +//! +//! Key details: +//! - POSIX follows PHP's single-quote and backslash escaping rules; Windows follows cmd.exe's +//! caret quoting and PHP's space-prefix rule for quotes, percent signs, and exclamation marks. +//! - Counted PHP strings are scanned before shell use so embedded NUL bytes throw `ValueError` +//! instead of being silently truncated by an operating-system command line. + +use crate::codegen_support::abi; +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::{Arch, Platform}; +use crate::codegen_support::runtime::data::{ + ESCAPE_SHELL_ARG_INPUT_LENGTH_MSG, ESCAPE_SHELL_ARG_NUL_MSG, + ESCAPE_SHELL_ARG_OUTPUT_LENGTH_MSG, ESCAPE_SHELL_CMD_INPUT_LENGTH_MSG, + ESCAPE_SHELL_CMD_NUL_MSG, ESCAPE_SHELL_CMD_OUTPUT_LENGTH_MSG, +}; + +/// Emits both platform-aware PHP shell escaping helpers. +pub fn emit_shell_escapes(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_shell_escapes_x86_64(emitter); + } else { + emit_shell_escapes_aarch64(emitter); + } +} + +/// Emits POSIX AArch64 shell escaping helpers using the internal string ABI. +fn emit_shell_escapes_aarch64(emitter: &mut Emitter) { + emit_escapeshellarg_aarch64(emitter); + emit_escapeshellcmd_aarch64(emitter); + emit_shell_utf8_sequence_len_aarch64(emitter); +} + +/// Emits the POSIX AArch64 `escapeshellarg()` helper. +fn emit_escapeshellarg_aarch64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: escapeshellarg (POSIX) ---"); + emitter.label_global("__rt_escapeshellarg"); + emitter.instruction("sub sp, sp, #48"); // reserve aligned storage for source metadata and the concat-or-heap result flag + emitter.instruction("stp x29, x30, [sp, #32]"); // preserve the caller frame while a heap fallback invokes the allocator + emitter.instruction("add x29, sp, #32"); // establish a stable frame base for the shell-argument helper + emitter.instruction("stp x1, x2, [sp]"); // preserve the counted PHP source across a possible heap allocation + emitter.instruction("mov x4, #4"); // a POSIX embedded quote expands one input byte to four output bytes + emitter.instruction("mul x5, x2, x4"); // compute the conservative escaped-payload capacity before writing any bytes + emitter.instruction("umulh x12, x2, x4"); // detect a capacity multiplication that does not fit in the machine word + emitter.instruction("cbnz x12, __rt_escapeshellarg_capacity_overflow"); // do not pass an already-wrapped conservative capacity to the allocator + emitter.instruction("adds x5, x5, #2"); // reserve the required opening and closing POSIX single quotes + emitter.instruction("b.cs __rt_escapeshellarg_capacity_overflow"); // do not pass an already-wrapped conservative capacity to the allocator + abi::emit_symbol_address(emitter, "x6", "_concat_off"); + emitter.instruction("ldr x7, [x6]"); // load the concat-buffer offset before quoting the shell argument + emitter.instruction("adds x12, x7, x5"); // calculate the concat-buffer end offset from the conservative capacity + emitter.instruction("b.cs __rt_escapeshellarg_heap"); // reject an offset addition that would wrap around the scratch allocation + emitter.instruction("mov x4, #65536"); // materialize the fixed concat scratch capacity in bytes + emitter.instruction("cmp x12, x4"); // does the worst-case escaped argument fit in scratch storage? + emitter.instruction("b.hi __rt_escapeshellarg_heap"); // use owned heap storage before any potentially overflowing write + abi::emit_symbol_address(emitter, "x8", "_concat_buf"); + emitter.instruction("add x8, x8, x7"); // select the next concat-buffer byte as the result start + emitter.instruction("mov x9, x8"); // retain the result start for the returned pointer and length + emitter.instruction("str xzr, [sp, #16]"); // record that this result consumes concat scratch on successful return + emitter.instruction("b __rt_escapeshellarg_start"); // skip the heap allocation branch when concat storage is sufficient + + emitter.label("__rt_escapeshellarg_heap"); + emitter.instruction("mov x0, x5"); // request the conservative escaped payload capacity from the runtime heap + emitter.instruction("bl __rt_heap_alloc"); // allocate owned storage so large shell arguments cannot overflow concat scratch + emitter.instruction("mov x4, #1"); // heap kind 1 marks an owned elephc string payload + emitter.instruction("str x4, [x0, #-8]"); // stamp the allocation before returning it as a string result + emitter.instruction("mov x8, x0"); // initialize the heap-backed destination cursor + emitter.instruction("mov x9, x0"); // retain the heap payload start for the returned string pair + emitter.instruction("mov x4, #1"); // mark this result as heap-backed for finalization + emitter.instruction("str x4, [sp, #16]"); // leave concat offset untouched for the heap-backed result + emitter.instruction("ldp x1, x2, [sp]"); // restore the counted PHP source after the allocator call + + emitter.label("__rt_escapeshellarg_start"); + emitter.instruction("mov x10, x2"); // retain the remaining counted source-byte length + emitter.instruction("mov w11, #39"); // materialize POSIX's enclosing single quote + emitter.instruction("strb w11, [x8], #1"); // write the opening shell-argument quote + + emitter.label("__rt_escapeshellarg_loop"); + emitter.instruction("cbz x10, __rt_escapeshellarg_done"); // finish after consuming the complete counted PHP string + emitter.instruction("mov x13, x1"); // preserve the source cursor while the bounded UTF-8 classifier uses argument registers + emitter.instruction("mov x0, x13"); // pass the current counted source cursor to the UTF-8 classifier + emitter.instruction("mov x1, x10"); // pass the bounded remaining byte count to the UTF-8 classifier + emitter.instruction("bl __rt_shell_utf8_sequence_len_aarch64"); // classify valid multibyte units before shell punctuation handling + emitter.instruction("mov x1, x13"); // restore the main source cursor after the helper call + emitter.instruction("cbnz x0, __rt_escapeshellarg_utf8_valid"); // preserve valid sequences atomically and discard only invalid leading bytes + emitter.instruction("add x1, x1, #1"); // skip one invalid leading byte like php_mblen() without interpreting it as shell syntax + emitter.instruction("sub x10, x10, #1"); // consume the skipped invalid byte from the counted source length + emitter.instruction("b __rt_escapeshellarg_loop"); // continue after discarding the invalid byte + emitter.label("__rt_escapeshellarg_utf8_valid"); + emitter.instruction("cmp x0, #1"); // does this valid sequence consist of one ASCII byte? + emitter.instruction("b.eq __rt_escapeshellarg_ascii"); // route ASCII through NUL and quote semantics + emitter.instruction("mov x12, x0"); // retain the valid multibyte sequence width for the copy loop + emitter.label("__rt_escapeshellarg_utf8_copy"); + emitter.instruction("ldrb w11, [x1], #1"); // load one already-validated UTF-8 byte from the counted source string + emitter.instruction("strb w11, [x8], #1"); // preserve the multibyte byte unchanged in the escaped result + emitter.instruction("sub x10, x10, #1"); // consume the copied byte from the bounded source length + emitter.instruction("subs x12, x12, #1"); // count down the validated byte sequence width + emitter.instruction("b.ne __rt_escapeshellarg_utf8_copy"); // copy every byte of the valid multibyte sequence atomically + emitter.instruction("b __rt_escapeshellarg_loop"); // continue at the next independent source sequence + emitter.label("__rt_escapeshellarg_ascii"); + emitter.instruction("ldrb w11, [x1], #1"); // load one ASCII argument byte and advance the counted source cursor + emitter.instruction("sub x10, x10, #1"); // account for the byte just consumed from the source string + emitter.instruction("cbz w11, __rt_escapeshellarg_nul"); // reject embedded NUL instead of truncating a future shell command + emitter.instruction("cmp w11, #255"); // does PHP's multibyte scanner reject this invalid byte? + emitter.instruction("b.eq __rt_escapeshellarg_loop"); // skip invalid 0xff bytes like php_escape_shell_arg() + emitter.instruction("cmp w11, #39"); // is the byte an embedded POSIX single quote? + emitter.instruction("b.ne __rt_escapeshellarg_copy"); // ordinary bytes remain inside the surrounding single quotes + emitter.instruction("mov w12, #39"); // materialize the quote closing the current literal segment + emitter.instruction("strb w12, [x8], #1"); // close the segment before the embedded quote + emitter.instruction("mov w12, #92"); // materialize the POSIX backslash escape separator + emitter.instruction("strb w12, [x8], #1"); // write the separator between quoted segments + emitter.instruction("mov w12, #39"); // materialize the quote character as its own literal segment + emitter.instruction("strb w12, [x8], #1"); // write the quoted single quote byte + emitter.instruction("mov w12, #39"); // reopen the enclosing single-quoted argument segment + emitter.instruction("strb w12, [x8], #1"); // continue the argument after the embedded quote + emitter.instruction("b __rt_escapeshellarg_loop"); // process the remaining argument bytes + + emitter.label("__rt_escapeshellarg_copy"); + emitter.instruction("strb w11, [x8], #1"); // copy an ordinary valid shell-argument byte unchanged + emitter.instruction("b __rt_escapeshellarg_loop"); // continue scanning the counted argument + + emitter.label("__rt_escapeshellarg_done"); + emitter.instruction("mov w11, #39"); // materialize the closing POSIX single quote + emitter.instruction("strb w11, [x8], #1"); // terminate the quoted shell argument + emitter.instruction("mov x1, x9"); // return the concat-backed escaped argument pointer + emitter.instruction("sub x2, x8, x9"); // return the escaped argument byte length + emitter.instruction("ldr x4, [sp, #16]"); // determine whether the returned payload is scratch-backed or heap-backed + emitter.instruction("cbnz x4, __rt_escapeshellarg_return"); // heap-backed results must not advance the shared concat offset + abi::emit_symbol_address(emitter, "x6", "_concat_off"); + emitter.instruction("ldr x7, [x6]"); // reload the concat offset before publishing this appended slice + emitter.instruction("add x7, x7, x2"); // advance concat storage by the full quoted result length + emitter.instruction("str x7, [x6]"); // publish the new concat-buffer offset for later string helpers + emitter.label("__rt_escapeshellarg_return"); + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore the caller frame after the shell argument has been materialized + emitter.instruction("add sp, sp, #48"); // release the shell-argument helper frame before returning + emitter.instruction("ret"); // return the escaped PHP string pair + + emitter.label("__rt_escapeshellarg_nul"); + emitter.instruction("ldr x4, [sp, #16]"); // determine whether the rejected argument owns a heap-backed partial result + emitter.instruction("cbz x4, __rt_escapeshellarg_nul_release"); // concat-backed partial output has no owned allocation to release + emitter.instruction("mov x0, x9"); // pass the owned partial-result payload to the heap releaser + emitter.instruction("bl __rt_heap_free"); // release heap-backed output before the catchable ValueError unwinds + emitter.label("__rt_escapeshellarg_nul_release"); + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore the caller frame before tail-entering the exception unwinder + emitter.instruction("add sp, sp, #48"); // release the shell-argument helper frame before throwing + emit_throw_value_error_aarch64(emitter, "_escapeshellarg_nul_msg", ESCAPE_SHELL_ARG_NUL_MSG.len()); + emitter.label("__rt_escapeshellarg_capacity_overflow"); + emitter.instruction("ldp x29, x30, [sp, #32]"); // release the helper frame before entering the non-returning allocation failure path + emitter.instruction("add sp, sp, #48"); // restore the caller stack before the fatal allocation tail edge + emitter.instruction("b __rt_heap_exhausted_entry"); // preserve the heap-fatal atom for this cross-helper non-returning tail +} + +/// Emits the POSIX AArch64 `escapeshellcmd()` helper. +fn emit_escapeshellcmd_aarch64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: escapeshellcmd (POSIX) ---"); + emitter.label_global("__rt_escapeshellcmd"); + emitter.instruction("sub sp, sp, #48"); // reserve an aligned frame for source restoration and the concat-or-heap result flag + emitter.instruction("stp x29, x30, [sp, #32]"); // preserve the caller frame while a large result uses the heap allocator + emitter.instruction("add x29, sp, #32"); // establish a stable frame base for the shell-command helper + emitter.instruction("stp x1, x2, [sp]"); // preserve the counted command source across a possible heap allocation + emitter.instruction("mov x4, #2"); // each POSIX command input byte can gain at most one backslash + emitter.instruction("mul x5, x2, x4"); // compute conservative escaped-command capacity before writing any bytes + emitter.instruction("umulh x12, x2, x4"); // detect a capacity multiplication that exceeds the native word size + emitter.instruction("cbnz x12, __rt_escapeshellcmd_capacity_overflow"); // do not pass an already-wrapped conservative capacity to the allocator + abi::emit_symbol_address(emitter, "x6", "_concat_off"); + emitter.instruction("ldr x7, [x6]"); // load the concat-buffer offset before escaping the command text + emitter.instruction("adds x12, x7, x5"); // calculate concat-buffer end offset from worst-case escaped-command capacity + emitter.instruction("b.cs __rt_escapeshellcmd_heap"); // avoid wrapping the concat offset before selecting scratch storage + emitter.instruction("mov x4, #65536"); // materialize the fixed concat scratch capacity in bytes + emitter.instruction("cmp x12, x4"); // does the worst-case result fit entirely in concat scratch? + emitter.instruction("b.hi __rt_escapeshellcmd_heap"); // allocate an owned result before any scratch-buffer overflow can occur + abi::emit_symbol_address(emitter, "x8", "_concat_buf"); + emitter.instruction("add x8, x8, x7"); // select the next concat-buffer byte as the command result start + emitter.instruction("mov x9, x8"); // retain the escaped command start for the return pair + emitter.instruction("str xzr, [sp, #16]"); // record that a successful result consumes concat scratch storage + emitter.instruction("b __rt_escapeshellcmd_start"); // skip allocation when concat scratch is provably sufficient + + emitter.label("__rt_escapeshellcmd_heap"); + emitter.instruction("mov x0, x5"); // request conservative escaped-command capacity from the runtime heap + emitter.instruction("bl __rt_heap_alloc"); // allocate owned storage so command escaping cannot overrun concat scratch + emitter.instruction("mov x4, #1"); // heap kind 1 marks an owned elephc string payload + emitter.instruction("str x4, [x0, #-8]"); // stamp the allocation before returning it as a string result + emitter.instruction("mov x8, x0"); // initialize the heap-backed command destination cursor + emitter.instruction("mov x9, x0"); // retain the heap payload start for the returned result pair + emitter.instruction("mov x4, #1"); // mark this result as heap-backed so concat offset remains unchanged + emitter.instruction("str x4, [sp, #16]"); // publish the storage kind for common finalization + emitter.instruction("ldp x1, x2, [sp]"); // restore the counted command source after allocator clobbers caller-saved registers + + emitter.label("__rt_escapeshellcmd_start"); + emitter.instruction("mov x10, x2"); // retain the remaining counted command byte length + emitter.instruction("mov x11, #0"); // clear the pending paired-quote source pointer + + emitter.label("__rt_escapeshellcmd_loop"); + emitter.instruction("cbz x10, __rt_escapeshellcmd_done"); // finish after processing every counted command byte + emitter.instruction("mov x13, x1"); // preserve the command cursor while the classifier uses argument registers + emitter.instruction("mov x0, x1"); // pass the current bounded command cursor to the UTF-8 classifier + emitter.instruction("mov x1, x10"); // pass the remaining counted command byte length to the UTF-8 classifier + emitter.instruction("bl __rt_shell_utf8_sequence_len_aarch64"); // classify a complete scalar before shell punctuation handling + emitter.instruction("mov x1, x13"); // restore the main command cursor after the classifier call + emitter.instruction("cbnz x0, __rt_escapeshellcmd_utf8_valid"); // preserve valid UTF-8 atomically and discard only invalid leading bytes + emitter.instruction("add x1, x1, #1"); // skip one invalid leading byte like php_mblen() without treating it as shell text + emitter.instruction("sub x10, x10, #1"); // consume the skipped invalid byte from the counted command length + emitter.instruction("b __rt_escapeshellcmd_loop"); // continue after discarding the invalid byte + emitter.label("__rt_escapeshellcmd_utf8_valid"); + emitter.instruction("cmp x0, #1"); // does this valid sequence consist of one ASCII byte? + emitter.instruction("b.eq __rt_escapeshellcmd_ascii"); // route ASCII through NUL, quote, and metacharacter handling + emitter.instruction("mov x14, x0"); // retain the valid multibyte sequence width for exact copying + emitter.label("__rt_escapeshellcmd_utf8_copy"); + emitter.instruction("ldrb w15, [x1], #1"); // load one already-validated UTF-8 byte from the counted command string + emitter.instruction("strb w15, [x8], #1"); // preserve the multibyte byte unchanged in the escaped command result + emitter.instruction("sub x10, x10, #1"); // consume the copied byte from the bounded command length + emitter.instruction("subs x14, x14, #1"); // count down the validated multibyte sequence width + emitter.instruction("b.ne __rt_escapeshellcmd_utf8_copy"); // copy every byte of the valid multibyte sequence atomically + emitter.instruction("b __rt_escapeshellcmd_loop"); // continue at the next independent command sequence + emitter.label("__rt_escapeshellcmd_ascii"); + emitter.instruction("ldrb w12, [x1], #1"); // load one ASCII command byte and advance the source cursor + emitter.instruction("sub x10, x10, #1"); // account for the consumed source byte + emitter.instruction("cbz w12, __rt_escapeshellcmd_nul"); // reject an embedded NUL before a shell could truncate the command + emitter.instruction("cmp w12, #255"); // does PHP's multibyte scanner reject this invalid byte? + emitter.instruction("b.eq __rt_escapeshellcmd_loop"); // skip invalid 0xff bytes like php_escape_shell_cmd() + emitter.instruction("cmp w12, #34"); // is this a double quote needing PHP's pairing rule? + emitter.instruction("b.eq __rt_escapeshellcmd_quote"); // pair or escape double quotes according to php-src + emitter.instruction("cmp w12, #39"); // is this a single quote needing PHP's pairing rule? + emitter.instruction("b.eq __rt_escapeshellcmd_quote"); // pair or escape single quotes according to php-src + emit_posix_shellcmd_special_checks_aarch64(emitter); + emitter.instruction("strb w12, [x8], #1"); // copy an ordinary command byte unchanged + emitter.instruction("b __rt_escapeshellcmd_loop"); // continue escaping the remaining command text + + emitter.label("__rt_escapeshellcmd_quote"); + emitter.instruction("cbz x11, __rt_escapeshellcmd_quote_scan_init"); // only an opening quote begins a bounded look-ahead scan + emitter.instruction("sub x14, x1, #1"); // recover the current quote's exact source address after consuming it + emitter.instruction("cmp x14, x11"); // is this quote the recorded endpoint rather than another quote type? + emitter.instruction("b.eq __rt_escapeshellcmd_quote_paired"); // only the recorded endpoint closes PHP's raw quote pair + emitter.instruction("b __rt_escapeshellcmd_quote_unpaired"); // escape an intervening quote without clearing the pending endpoint + emitter.label("__rt_escapeshellcmd_quote_scan_init"); + emitter.instruction("mov x13, x1"); // begin a bounded forward scan after this quote + emitter.instruction("mov x14, x10"); // preserve the number of following source bytes to inspect + emitter.label("__rt_escapeshellcmd_quote_scan"); + emitter.instruction("cbz x14, __rt_escapeshellcmd_quote_unpaired"); // no matching quote means this quote must gain a backslash + emitter.instruction("ldrb w15, [x13]"); // inspect the next byte without consuming the main source cursor + emitter.instruction("cmp w15, w12"); // does it match this quote's exact delimiter byte? + emitter.instruction("b.eq __rt_escapeshellcmd_quote_found"); // remember the matching endpoint and preserve both quotes raw + emitter.instruction("add x13, x13, #1"); // advance the look-ahead cursor past a non-matching byte + emitter.instruction("sub x14, x14, #1"); // consume one bounded look-ahead byte + emitter.instruction("b __rt_escapeshellcmd_quote_scan"); // keep searching for the matching quote + emitter.label("__rt_escapeshellcmd_quote_found"); + emitter.instruction("mov x11, x13"); // remember the matching quote source address for its later iteration + emitter.instruction("strb w12, [x8], #1"); // copy the first quote raw because PHP found its matching partner + emitter.instruction("b __rt_escapeshellcmd_loop"); // resume normal command-byte processing + emitter.label("__rt_escapeshellcmd_quote_paired"); + emitter.instruction("mov x11, #0"); // clear the pair marker after reaching the matching quote + emitter.instruction("strb w12, [x8], #1"); // copy the matching quote raw like PHP's paired-quote case + emitter.instruction("b __rt_escapeshellcmd_loop"); // resume normal command-byte processing + emitter.label("__rt_escapeshellcmd_quote_unpaired"); + emitter.instruction("mov w15, #92"); // materialize a POSIX shell backslash before an unpaired quote + emitter.instruction("strb w15, [x8], #1"); // escape the unpaired quote for the POSIX shell + emitter.instruction("strb w12, [x8], #1"); // write the original unpaired quote after its escape + emitter.instruction("b __rt_escapeshellcmd_loop"); // continue with the remaining command bytes + + emitter.label("__rt_escapeshellcmd_escape"); + emitter.instruction("mov w15, #92"); // materialize the POSIX metacharacter escape prefix + emitter.instruction("strb w15, [x8], #1"); // escape this command metacharacter for the POSIX shell + emitter.instruction("strb w12, [x8], #1"); // write the original escaped command metacharacter + emitter.instruction("b __rt_escapeshellcmd_loop"); // process the next command byte + + emitter.label("__rt_escapeshellcmd_done"); + emitter.instruction("mov x1, x9"); // return the concat-backed escaped command pointer + emitter.instruction("sub x2, x8, x9"); // return the escaped command byte length + emitter.instruction("ldr x4, [sp, #16]"); // determine whether this result is scratch-backed or heap-backed + emitter.instruction("cbnz x4, __rt_escapeshellcmd_return"); // heap-backed command results must leave concat offset unchanged + abi::emit_symbol_address(emitter, "x6", "_concat_off"); + emitter.instruction("ldr x7, [x6]"); // reload the concat offset before publishing the command result + emitter.instruction("add x7, x7, x2"); // advance concat storage by the escaped command length + emitter.instruction("str x7, [x6]"); // publish the new concat-buffer offset + emitter.label("__rt_escapeshellcmd_return"); + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore the caller frame after materializing the escaped command + emitter.instruction("add sp, sp, #48"); // release the shell-command helper frame before returning + emitter.instruction("ret"); // return the escaped command string pair + + emitter.label("__rt_escapeshellcmd_nul"); + emitter.instruction("ldr x4, [sp, #16]"); // determine whether the rejected command owns a heap-backed partial result + emitter.instruction("cbz x4, __rt_escapeshellcmd_nul_release"); // concat-backed partial output has no owned allocation to release + emitter.instruction("mov x0, x9"); // pass the owned partial-result payload to the heap releaser + emitter.instruction("bl __rt_heap_free"); // release heap-backed output before the catchable ValueError unwinds + emitter.label("__rt_escapeshellcmd_nul_release"); + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore the caller frame before tail-entering the exception unwinder + emitter.instruction("add sp, sp, #48"); // release the shell-command helper frame before throwing + emit_throw_value_error_aarch64(emitter, "_escapeshellcmd_nul_msg", ESCAPE_SHELL_CMD_NUL_MSG.len()); + emitter.label("__rt_escapeshellcmd_capacity_overflow"); + emitter.instruction("ldp x29, x30, [sp, #32]"); // release the helper frame before entering the non-returning allocation failure path + emitter.instruction("add sp, sp, #48"); // restore the caller stack before the fatal allocation tail edge + emitter.instruction("b __rt_heap_exhausted_entry"); // preserve the heap-fatal atom for this cross-helper non-returning tail +} + +/// Emits POSIX `escapeshellcmd()` metacharacter comparisons for one loaded byte. +fn emit_posix_shellcmd_special_checks_aarch64(emitter: &mut Emitter) { + for byte in [35, 38, 59, 96, 124, 42, 63, 126, 60, 62, 94, 40, 41, 91, 93, 123, 125, 36, 92, 10] { + emitter.instruction(&format!("cmp w12, #{byte}")); // test one PHP POSIX shell metacharacter requiring a backslash prefix + emitter.instruction("b.eq __rt_escapeshellcmd_escape"); // branch when the current byte must be escaped for the POSIX shell + } +} + +/// Emits a bounded, scalar-validating UTF-8 sequence classifier for AArch64 shell helpers. +/// +/// Input is `x0` = source cursor and `x1` = remaining byte count; it returns a sequence +/// width in `x0`, or zero when PHP must discard exactly one invalid leading byte. +fn emit_shell_utf8_sequence_len_aarch64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: bounded UTF-8 sequence classifier ---"); + emitter.label_global("__rt_shell_utf8_sequence_len_aarch64"); + emitter.instruction("cbz x1, __rt_shell_utf8_invalid_aarch64"); // an empty bounded suffix has no UTF-8 leading sequence + emitter.instruction("ldrb w2, [x0]"); // load the candidate leader without traversing a C string + emitter.instruction("cmp w2, #128"); // is the leading byte ordinary ASCII, including NUL? + emitter.instruction("b.lo __rt_shell_utf8_one_aarch64"); // ASCII consumes exactly one byte and receives shell-specific handling + emitter.instruction("cmp w2, #194"); // reject continuation bytes and overlong two-byte leaders + emitter.instruction("b.lo __rt_shell_utf8_invalid_aarch64"); // PHP discards one malformed leading byte at a time + emitter.instruction("cmp w2, #223"); // is this a valid two-byte UTF-8 leader? + emitter.instruction("b.ls __rt_shell_utf8_two_aarch64"); // validate one bounded continuation byte + emitter.instruction("cmp w2, #239"); // is this a valid three-byte UTF-8 leader? + emitter.instruction("b.ls __rt_shell_utf8_three_aarch64"); // validate two bounded continuation bytes and scalar boundaries + emitter.instruction("cmp w2, #244"); // Unicode accepts at most the F4 four-byte leader + emitter.instruction("b.hi __rt_shell_utf8_invalid_aarch64"); // reject impossible leaders before reading continuation bytes + emitter.instruction("cmp x1, #4"); // are all four bytes within the counted PHP string? + emitter.instruction("b.lo __rt_shell_utf8_invalid_aarch64"); // reject truncated four-byte units without an out-of-bounds read + emitter.instruction("ldrb w3, [x0, #1]"); // inspect the first four-byte continuation candidate + emitter.instruction("and w4, w3, #192"); // isolate its UTF-8 continuation bit pattern + emitter.instruction("cmp w4, #128"); // is the first four-byte continuation structurally valid? + emitter.instruction("b.ne __rt_shell_utf8_invalid_aarch64"); // reject malformed four-byte continuation bytes + emitter.instruction("cmp w2, #240"); // F0 needs a non-overlong first continuation + emitter.instruction("b.ne __rt_shell_utf8_four_f4_aarch64"); // only F0 has the lower scalar boundary + emitter.instruction("cmp w3, #144"); // must F0's continuation be at least 0x90? + emitter.instruction("b.lo __rt_shell_utf8_invalid_aarch64"); // reject an overlong four-byte scalar + emitter.label("__rt_shell_utf8_four_f4_aarch64"); + emitter.instruction("cmp w2, #244"); // F4 needs the Unicode upper scalar boundary + emitter.instruction("b.ne __rt_shell_utf8_four_rest_aarch64"); // non-F4 leaders have no additional upper bound here + emitter.instruction("cmp w3, #143"); // must F4's continuation be at most 0x8f? + emitter.instruction("b.hi __rt_shell_utf8_invalid_aarch64"); // reject scalars above U+10FFFF + emitter.label("__rt_shell_utf8_four_rest_aarch64"); + emit_shell_utf8_continuation_check_aarch64(emitter, 2, "__rt_shell_utf8_invalid_aarch64"); + emit_shell_utf8_continuation_check_aarch64(emitter, 3, "__rt_shell_utf8_invalid_aarch64"); + emitter.instruction("mov x0, #4"); // report one valid four-byte UTF-8 scalar sequence + emitter.instruction("ret"); // return while leaving outer shell-loop registers untouched + + emitter.label("__rt_shell_utf8_three_aarch64"); + emitter.instruction("cmp x1, #3"); // are both continuation bytes inside the counted source suffix? + emitter.instruction("b.lo __rt_shell_utf8_invalid_aarch64"); // reject a truncated three-byte UTF-8 sequence + emitter.instruction("ldrb w3, [x0, #1]"); // inspect the first three-byte continuation candidate + emitter.instruction("and w4, w3, #192"); // isolate its UTF-8 continuation bit pattern + emitter.instruction("cmp w4, #128"); // is the first three-byte continuation structurally valid? + emitter.instruction("b.ne __rt_shell_utf8_invalid_aarch64"); // reject malformed three-byte continuation bytes + emitter.instruction("cmp w2, #224"); // E0 needs a non-overlong first continuation + emitter.instruction("b.ne __rt_shell_utf8_three_ed_aarch64"); // only E0 has the lower scalar boundary + emitter.instruction("cmp w3, #160"); // must E0's continuation be at least 0xa0? + emitter.instruction("b.lo __rt_shell_utf8_invalid_aarch64"); // reject an overlong three-byte scalar + emitter.label("__rt_shell_utf8_three_ed_aarch64"); + emitter.instruction("cmp w2, #237"); // ED starts the UTF-16 surrogate range boundary + emitter.instruction("b.ne __rt_shell_utf8_three_rest_aarch64"); // non-ED leaders need no additional upper bound here + emitter.instruction("cmp w3, #159"); // must ED's continuation be at most 0x9f? + emitter.instruction("b.hi __rt_shell_utf8_invalid_aarch64"); // reject UTF-16 surrogate code points + emitter.label("__rt_shell_utf8_three_rest_aarch64"); + emit_shell_utf8_continuation_check_aarch64(emitter, 2, "__rt_shell_utf8_invalid_aarch64"); + emitter.instruction("mov x0, #3"); // report one valid three-byte UTF-8 scalar sequence + emitter.instruction("ret"); // return while leaving outer shell-loop registers untouched + + emitter.label("__rt_shell_utf8_two_aarch64"); + emitter.instruction("cmp x1, #2"); // is the continuation byte inside the counted source suffix? + emitter.instruction("b.lo __rt_shell_utf8_invalid_aarch64"); // reject a truncated two-byte UTF-8 sequence + emit_shell_utf8_continuation_check_aarch64(emitter, 1, "__rt_shell_utf8_invalid_aarch64"); + emitter.instruction("mov x0, #2"); // report one valid two-byte UTF-8 scalar sequence + emitter.instruction("ret"); // return while leaving outer shell-loop registers untouched + + emitter.label("__rt_shell_utf8_one_aarch64"); + emitter.instruction("mov x0, #1"); // report an ASCII sequence for shell-specific classification + emitter.instruction("ret"); // return while leaving outer shell-loop registers untouched + emitter.label("__rt_shell_utf8_invalid_aarch64"); + emitter.instruction("mov x0, #0"); // request that callers discard exactly one invalid leading byte + emitter.instruction("ret"); // return while leaving outer shell-loop registers untouched +} + +/// Emits one bounded continuation-byte check for the AArch64 shell UTF-8 classifier. +fn emit_shell_utf8_continuation_check_aarch64(emitter: &mut Emitter, offset: usize, invalid_label: &str) { + emitter.instruction(&format!("ldrb w3, [x0, #{offset}]")); // load one bounded UTF-8 continuation candidate from the source slice + emitter.instruction("and w4, w3, #192"); // isolate its required continuation bit pattern + emitter.instruction("cmp w4, #128"); // does this byte have the 10xxxxxx continuation form? + emitter.instruction(&format!("b.ne {invalid_label}")); // reject the whole sequence when any continuation is malformed +} + +/// Emits the x86_64 internal-ABI shell escape helpers, selecting Windows semantics when needed. +fn emit_shell_escapes_x86_64(emitter: &mut Emitter) { + emit_escapeshellarg_x86_64(emitter, emitter.target.platform == Platform::Windows); + emit_escapeshellcmd_x86_64(emitter, emitter.target.platform == Platform::Windows); + emit_shell_utf8_sequence_len_x86_64(emitter); +} + +/// Emits x86_64 `escapeshellarg()` using either PHP's POSIX or Windows rule. +fn emit_escapeshellarg_x86_64(emitter: &mut Emitter, windows: bool) { + emitter.blank(); + emitter.comment(if windows { "--- runtime: escapeshellarg (Windows) ---" } else { "--- runtime: escapeshellarg (POSIX) ---" }); + emitter.label_global("__rt_escapeshellarg"); + emitter.instruction("push rbp"); // preserve the caller frame while a large shell argument falls back to heap storage + emitter.instruction("mov rbp, rsp"); // establish stable spill slots for source metadata and storage kind + emitter.instruction("sub rsp, 32"); // reserve aligned storage across the heap allocator call + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // preserve the counted source pointer across a possible allocation + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // preserve the counted source length across a possible allocation + if windows { + emitter.instruction("cmp rdx, 8189"); // php-src Windows accepts at most cmd_max_len minus two quotes and one NUL byte + emitter.instruction("ja __rt_escapeshellarg_input_too_long_x86"); // throw the exact PHP ValueError before reserving or writing output storage + } + emitter.instruction("mov r11, rdx"); // seed conservative escaped capacity from the input byte length + emitter.instruction(if windows { "imul r11, 2" } else { "imul r11, 4" }); // account for the platform's largest per-byte escape expansion + emitter.instruction("jo __rt_escapeshellarg_capacity_overflow_x86"); // never pass a wrapped conservative capacity to the allocator + emitter.instruction(if windows { "add r11, 3" } else { "add r11, 2" }); // reserve surrounding quotes and Windows's possible trailing slash guard + emitter.instruction("jc __rt_escapeshellarg_capacity_overflow_x86"); // never pass a wrapped conservative capacity to the allocator + abi::emit_symbol_address(emitter, "r8", "_concat_off"); + emitter.instruction("mov r9, QWORD PTR [r8]"); // load the concat-buffer offset before quoting the shell argument + emitter.instruction("mov r8, r9"); // retain the current concat offset while checking conservative capacity + emitter.instruction("add r8, r11"); // calculate the concat scratch end offset before any output write + emitter.instruction("jc __rt_escapeshellarg_heap_x86"); // use owned storage rather than wrapping concat scratch addressing + emitter.instruction("cmp r8, 65536"); // does the worst-case escaped argument fit in the fixed concat scratch buffer? + emitter.instruction("ja __rt_escapeshellarg_heap_x86"); // allocate an owned string before a scratch-buffer overflow can occur + abi::emit_symbol_address(emitter, "r10", "_concat_buf"); + emitter.instruction("add r10, r9"); // select the concat-buffer destination for the escaped argument + emitter.instruction("mov r9, r10"); // retain the escaped-argument start pointer for the result pair + emitter.instruction("mov QWORD PTR [rbp - 24], 0"); // mark the result as concat-backed for final offset publication + emitter.instruction("jmp __rt_escapeshellarg_start_x86"); // skip heap allocation when scratch storage is sufficient + + emitter.label("__rt_escapeshellarg_heap_x86"); + emitter.instruction("mov rax, r11"); // request conservative escaped payload capacity from the runtime heap + emitter.instruction("call __rt_heap_alloc"); // allocate owned output storage for an argument that exceeds concat scratch + emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(1))); // materialize the owned-string heap kind word with the x86_64 marker + emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the heap allocation as a managed string payload + emitter.instruction("mov r10, rax"); // initialize the heap-backed output cursor + emitter.instruction("mov r9, rax"); // retain the heap payload start for the returned result pair + emitter.instruction("mov QWORD PTR [rbp - 24], 1"); // prevent heap-backed results from advancing concat offset + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // restore the counted source cursor after the allocator call + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // restore the counted source length after the allocator call + + emitter.label("__rt_escapeshellarg_start_x86"); + emitter.instruction("mov rcx, rdx"); // retain the remaining counted source-byte length + emitter.instruction(if windows { "mov BYTE PTR [r10], 34" } else { "mov BYTE PTR [r10], 39" }); // write PHP's platform-specific opening argument quote + emitter.instruction("add r10, 1"); // advance past the opening quote in concat storage + if windows { + emitter.instruction("xor r11d, r11d"); // reset the trailing-backslash run counter for Windows closing-quote protection + } + + emitter.label("__rt_escapeshellarg_loop_x86"); + emitter.instruction("test rcx, rcx"); // finish once every byte in the counted PHP argument has been processed + emitter.instruction("jz __rt_escapeshellarg_done_x86"); // finalize the quoted argument after the source cursor reaches its end + emitter.instruction("mov rdi, rax"); // pass the current source cursor to the bounded UTF-8 sequence validator + emitter.instruction("mov rsi, rcx"); // pass the remaining counted source length to the UTF-8 validator + emitter.instruction("call __rt_shell_utf8_sequence_len"); // classify one valid UTF-8 sequence without C-string scanning + emitter.instruction("test eax, eax"); // did the current byte begin a valid UTF-8 sequence? + emitter.instruction("jnz __rt_escapeshellarg_utf8_valid_x86"); // copy valid sequences atomically so continuation bytes never become shell syntax + emitter.instruction("mov rax, rdi"); // restore the source cursor before skipping the invalid leading UTF-8 byte + emitter.instruction("add rax, 1"); // skip exactly one invalid leading UTF-8 byte like php_mblen() + emitter.instruction("sub rcx, 1"); // consume the invalid byte from the bounded PHP string + emitter.instruction("jmp __rt_escapeshellarg_loop_x86"); // continue after discarding the invalid byte + emitter.label("__rt_escapeshellarg_utf8_valid_x86"); + emitter.instruction("mov r8d, eax"); // retain the validated UTF-8 sequence width for source and output advancement + emitter.instruction("mov rax, rdi"); // restore the source cursor after using rax for the validator result + emitter.instruction("cmp r8d, 1"); // is this an ASCII byte needing shell-specific classification? + emitter.instruction("je __rt_escapeshellarg_ascii_x86"); // route ASCII through quote and NUL handling + emitter.instruction("mov rsi, rax"); // begin copying the validated multibyte UTF-8 sequence unchanged + emitter.label("__rt_escapeshellarg_utf8_copy_x86"); + emitter.instruction("mov al, BYTE PTR [rsi]"); // load one already-validated UTF-8 byte for verbatim output + emitter.instruction("add rsi, 1"); // advance the bounded source cursor through the UTF-8 sequence + emitter.instruction("mov BYTE PTR [r10], al"); // preserve the UTF-8 byte exactly in the escaped PHP string + emitter.instruction("add r10, 1"); // advance concat storage after the copied UTF-8 byte + emitter.instruction("sub rcx, 1"); // consume the copied UTF-8 byte from the source length + emitter.instruction("sub r8, 1"); // consume one byte from the validated sequence width + emitter.instruction("jnz __rt_escapeshellarg_utf8_copy_x86"); // copy every byte in the valid multibyte sequence + emitter.instruction("mov rax, rsi"); // publish the source cursor after the complete UTF-8 sequence + emitter.instruction("jmp __rt_escapeshellarg_loop_x86"); // continue with the next independent source sequence + emitter.label("__rt_escapeshellarg_ascii_x86"); + emitter.instruction("mov dl, BYTE PTR [rax]"); // load the next source byte without using C-string termination + emitter.instruction("add rax, 1"); // advance the counted PHP string cursor after consuming the byte + emitter.instruction("sub rcx, 1"); // decrement the number of source bytes still to process + emitter.instruction("test dl, dl"); // would this byte truncate an OS-level command string? + emitter.instruction("jz __rt_escapeshellarg_nul_x86"); // raise PHP's ValueError for an embedded NUL byte + if windows { + emitter.instruction("cmp dl, 92"); // is this byte a backslash that may precede the closing quote? + emitter.instruction("jne __rt_escapeshellarg_not_backslash_x86"); // reset the trailing-backslash run for every non-backslash byte + emitter.instruction("add r11, 1"); // count this trailing backslash for PHP's Windows quote rule + emitter.instruction("jmp __rt_escapeshellarg_windows_special_x86"); // preserve a backslash after recording its current run length + emitter.label("__rt_escapeshellarg_not_backslash_x86"); + emitter.instruction("xor r11d, r11d"); // a non-backslash breaks the trailing run before the final quote + emitter.label("__rt_escapeshellarg_windows_special_x86"); + emitter.instruction("cmp dl, 34"); // does PHP replace a double quote with a leading space on Windows? + emitter.instruction("je __rt_escapeshellarg_windows_space_x86"); // apply the Windows quote replacement rule + emitter.instruction("cmp dl, 37"); // does PHP replace a percent sign with a leading space on Windows? + emitter.instruction("je __rt_escapeshellarg_windows_space_x86"); // apply the Windows environment-expansion protection rule + emitter.instruction("cmp dl, 33"); // does PHP replace an exclamation mark with a leading space on Windows? + emitter.instruction("je __rt_escapeshellarg_windows_space_x86"); // apply the delayed-expansion protection rule + emitter.instruction("jmp __rt_escapeshellarg_copy_x86"); // ordinary Windows argument bytes pass through unchanged + emitter.label("__rt_escapeshellarg_windows_space_x86"); + emitter.instruction("mov BYTE PTR [r10], 32"); // replace quote, percent, or exclamation with PHP's Windows replacement space + emitter.instruction("add r10, 1"); // advance concat storage after the replacement space + emitter.instruction("jmp __rt_escapeshellarg_loop_x86"); // discard the replaced byte and continue with the next source character + } else { + emitter.instruction("cmp dl, 39"); // is this byte an embedded POSIX single quote? + emitter.instruction("jne __rt_escapeshellarg_copy_x86"); // ordinary bytes remain inside the enclosing POSIX quotes + emitter.instruction("mov BYTE PTR [r10], 39"); // close the current POSIX single-quoted argument segment + emitter.instruction("mov BYTE PTR [r10 + 1], 92"); // write the backslash separator between quote segments + emitter.instruction("mov BYTE PTR [r10 + 2], 39"); // write the embedded quote as its own single-quoted segment + emitter.instruction("mov BYTE PTR [r10 + 3], 39"); // reopen the enclosing POSIX quoted segment + emitter.instruction("add r10, 4"); // advance past PHP's four-byte embedded-quote expansion + emitter.instruction("jmp __rt_escapeshellarg_loop_x86"); // continue after expanding the embedded POSIX quote + } + + emitter.label("__rt_escapeshellarg_copy_x86"); + emitter.instruction("mov BYTE PTR [r10], dl"); // copy an ordinary valid argument byte into the concat-backed result + emitter.instruction("add r10, 1"); // advance concat storage after copying the argument byte + emitter.instruction("jmp __rt_escapeshellarg_loop_x86"); // process the remaining counted source bytes + + emitter.label("__rt_escapeshellarg_done_x86"); + if windows { + emitter.instruction("test r11b, 1"); // does an odd run of trailing backslashes precede the closing quote? + emitter.instruction("jz __rt_escapeshellarg_close_x86"); // an even run already leaves the closing quote unescaped + emitter.instruction("mov BYTE PTR [r10], 92"); // double the odd trailing backslash run before closing the Windows argument + emitter.instruction("add r10, 1"); // advance concat storage after the protective extra backslash + emitter.label("__rt_escapeshellarg_close_x86"); + emitter.instruction("mov BYTE PTR [r10], 34"); // terminate the Windows shell argument with a double quote + } else { + emitter.instruction("mov BYTE PTR [r10], 39"); // terminate the POSIX shell argument with a single quote + } + emitter.instruction("add r10, 1"); // advance past the closing platform-specific quote + emitter.instruction("mov rdx, r10"); // snapshot the final destination cursor before calculating the result length + emitter.instruction("sub rdx, r9"); // calculate the escaped argument length from cursor minus result start + if windows { + emitter.instruction("cmp rdx, 8193"); // php-src rejects escaped Windows arguments longer than cmd_max_len plus its trailing NUL + emitter.instruction("ja __rt_escapeshellarg_output_too_long_x86"); // throw the exact PHP ValueError instead of returning an over-limit command argument + } + emitter.instruction("mov rax, r9"); // return either concat-backed or heap-backed result start in the string result register + emitter.instruction("cmp QWORD PTR [rbp - 24], 0"); // determine whether this result is concat-backed or heap-backed + emitter.instruction("jne __rt_escapeshellarg_return_x86"); // heap-backed results must leave the shared concat offset unchanged + abi::emit_load_symbol_to_reg(emitter, "r8", "_concat_off", 0); // reload the previous concat offset before publishing the appended argument + emitter.instruction("add r8, rdx"); // advance the concat offset by the full escaped argument length + abi::emit_store_reg_to_symbol(emitter, "r8", "_concat_off", 0); // publish the new concat offset for later string results + emitter.label("__rt_escapeshellarg_return_x86"); + emitter.instruction("mov rsp, rbp"); // release local shell-argument spill slots before restoring the caller frame + emitter.instruction("pop rbp"); // restore the caller frame pointer before returning the escaped argument + emitter.instruction("ret"); // return the escaped argument pointer and length pair + + emitter.label("__rt_escapeshellarg_nul_x86"); + emitter.instruction("cmp QWORD PTR [rbp - 24], 0"); // determine whether the rejected argument owns a heap-backed partial result + emitter.instruction("je __rt_escapeshellarg_nul_release_x86"); // concat-backed partial output has no owned allocation to release + emitter.instruction("mov rax, r9"); // pass the owned partial-result payload to the heap releaser + emitter.instruction("call __rt_heap_free"); // release heap-backed output before the catchable ValueError unwinds + emitter.label("__rt_escapeshellarg_nul_release_x86"); + emitter.instruction("mov rsp, rbp"); // release the helper frame before tail-entering the catchable exception unwinder + emitter.instruction("pop rbp"); // restore the caller frame pointer before the catchable ValueError tail edge + emit_throw_value_error_x86_64(emitter, "_escapeshellarg_nul_msg", ESCAPE_SHELL_ARG_NUL_MSG.len()); + if windows { + emitter.label("__rt_escapeshellarg_input_too_long_x86"); + emitter.instruction("mov rsp, rbp"); // release the helper frame before tail-entering the catchable exception unwinder + emitter.instruction("pop rbp"); // restore the caller frame pointer before the catchable ValueError tail edge + emit_throw_value_error_x86_64( + emitter, + "_escapeshellarg_input_length_msg", + ESCAPE_SHELL_ARG_INPUT_LENGTH_MSG.len(), + ); + emitter.label("__rt_escapeshellarg_output_too_long_x86"); + emitter.instruction("cmp QWORD PTR [rbp - 24], 0"); // determine whether the rejected argument owns a heap-backed completed result + emitter.instruction("je __rt_escapeshellarg_output_release_x86"); // concat-backed output has no owned allocation to release + emitter.instruction("mov rax, r9"); // pass the owned completed-result payload to the heap releaser + emitter.instruction("call __rt_heap_free"); // release heap-backed output before the catchable ValueError unwinds + emitter.label("__rt_escapeshellarg_output_release_x86"); + emitter.instruction("mov rsp, rbp"); // release the helper frame before tail-entering the catchable exception unwinder + emitter.instruction("pop rbp"); // restore the caller frame pointer before the catchable ValueError tail edge + emit_throw_value_error_x86_64( + emitter, + "_escapeshellarg_output_length_msg", + ESCAPE_SHELL_ARG_OUTPUT_LENGTH_MSG.len(), + ); + } + emitter.label("__rt_escapeshellarg_capacity_overflow_x86"); + emitter.instruction("mov rsp, rbp"); // release the helper frame before entering the non-returning allocation failure path + emitter.instruction("pop rbp"); // restore the caller frame pointer before the fatal allocation tail edge + emitter.instruction("jmp __rt_heap_exhausted_entry"); // preserve the heap-fatal atom for this cross-helper non-returning tail +} + +/// Emits x86_64 `escapeshellcmd()` using either PHP's POSIX or Windows metacharacter rule. +fn emit_escapeshellcmd_x86_64(emitter: &mut Emitter, windows: bool) { + emitter.blank(); + emitter.comment(if windows { "--- runtime: escapeshellcmd (Windows) ---" } else { "--- runtime: escapeshellcmd (POSIX) ---" }); + emitter.label_global("__rt_escapeshellcmd"); + emitter.instruction("push rbp"); // preserve the caller frame while a large command result falls back to heap storage + emitter.instruction("mov rbp, rsp"); // establish stable spill slots for source metadata and storage kind + emitter.instruction("sub rsp, 32"); // reserve aligned storage across the heap allocator call + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // preserve the counted command source pointer across a possible allocation + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // preserve the counted command length across a possible allocation + if windows { + emitter.instruction("cmp rdx, 8189"); // php-src Windows accepts at most cmd_max_len minus two quotes and one NUL byte + emitter.instruction("ja __rt_escapeshellcmd_input_too_long_x86"); // throw the exact PHP ValueError before reserving or writing output storage + } + emitter.instruction("mov r11, rdx"); // seed conservative escaped capacity from the command byte length + emitter.instruction("imul r11, 2"); // every shell command byte can gain at most one escape prefix + emitter.instruction("jo __rt_escapeshellcmd_capacity_overflow_x86"); // never pass a wrapped conservative capacity to the allocator + abi::emit_symbol_address(emitter, "r8", "_concat_off"); + emitter.instruction("mov r9, QWORD PTR [r8]"); // load the concat-buffer offset before escaping the command string + emitter.instruction("mov r8, r9"); // retain the current concat offset while checking conservative capacity + emitter.instruction("add r8, r11"); // calculate the concat scratch end offset before any output write + emitter.instruction("jc __rt_escapeshellcmd_heap_x86"); // use owned storage rather than wrapping concat scratch addressing + emitter.instruction("cmp r8, 65536"); // does the worst-case escaped command fit in the fixed concat scratch buffer? + emitter.instruction("ja __rt_escapeshellcmd_heap_x86"); // allocate an owned string before a scratch-buffer overflow can occur + abi::emit_symbol_address(emitter, "r10", "_concat_buf"); + emitter.instruction("add r10, r9"); // select the concat-buffer destination for the escaped command + emitter.instruction("mov r9, r10"); // retain the escaped-command start pointer for the result pair + emitter.instruction("mov QWORD PTR [rbp - 24], 0"); // mark the result as concat-backed for final offset publication + emitter.instruction("jmp __rt_escapeshellcmd_start_x86"); // skip heap allocation when scratch storage is sufficient + + emitter.label("__rt_escapeshellcmd_heap_x86"); + emitter.instruction("mov rax, r11"); // request conservative escaped payload capacity from the runtime heap + emitter.instruction("call __rt_heap_alloc"); // allocate owned output storage for a command that exceeds concat scratch + emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(1))); // materialize the owned-string heap kind word with the x86_64 marker + emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the heap allocation as a managed string payload + emitter.instruction("mov r10, rax"); // initialize the heap-backed output cursor + emitter.instruction("mov r9, rax"); // retain the heap payload start for the returned result pair + emitter.instruction("mov QWORD PTR [rbp - 24], 1"); // prevent heap-backed results from advancing concat offset + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // restore the counted command source cursor after the allocator call + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // restore the counted command length after the allocator call + + emitter.label("__rt_escapeshellcmd_start_x86"); + emitter.instruction("mov rcx, rdx"); // retain the remaining counted command byte length + if !windows { + emitter.instruction("xor r11d, r11d"); // clear the pending matching-quote pointer for PHP's POSIX pairing rule + } + + emitter.label("__rt_escapeshellcmd_loop_x86"); + emitter.instruction("test rcx, rcx"); // finish once every byte in the counted command string has been processed + emitter.instruction("jz __rt_escapeshellcmd_done_x86"); // finalize the escaped command after the source cursor reaches its end + emitter.instruction("mov rdi, rax"); // pass the current command cursor to the bounded UTF-8 sequence validator + emitter.instruction("mov rsi, rcx"); // pass the remaining counted command length to the UTF-8 validator + emitter.instruction("call __rt_shell_utf8_sequence_len"); // classify one valid UTF-8 sequence without treating invalid bytes as shell text + emitter.instruction("test eax, eax"); // did the command cursor begin a valid UTF-8 sequence? + emitter.instruction("jnz __rt_escapeshellcmd_utf8_valid_x86"); // preserve valid sequences and discard only invalid leading bytes + emitter.instruction("mov rax, rdi"); // restore the command cursor before skipping the invalid leading UTF-8 byte + emitter.instruction("add rax, 1"); // skip exactly one invalid leading UTF-8 byte like php_mblen() + emitter.instruction("sub rcx, 1"); // consume the invalid byte from the bounded command string + emitter.instruction("jmp __rt_escapeshellcmd_loop_x86"); // continue after discarding the invalid byte + emitter.label("__rt_escapeshellcmd_utf8_valid_x86"); + emitter.instruction("mov r8d, eax"); // retain the validated UTF-8 sequence width for source and output advancement + emitter.instruction("mov rax, rdi"); // restore the command source cursor after validation + emitter.instruction("cmp r8d, 1"); // is this an ASCII byte requiring shell metacharacter classification? + emitter.instruction("je __rt_escapeshellcmd_ascii_x86"); // route ASCII through quote, NUL, and metacharacter handling + emitter.instruction("mov rsi, rax"); // begin copying the validated multibyte UTF-8 sequence unchanged + emitter.label("__rt_escapeshellcmd_utf8_copy_x86"); + emitter.instruction("mov al, BYTE PTR [rsi]"); // load one already-validated UTF-8 byte for verbatim command output + emitter.instruction("add rsi, 1"); // advance the bounded source cursor through the UTF-8 sequence + emitter.instruction("mov BYTE PTR [r10], al"); // preserve the UTF-8 byte exactly in the escaped command + emitter.instruction("add r10, 1"); // advance concat storage after the copied UTF-8 byte + emitter.instruction("sub rcx, 1"); // consume the copied UTF-8 byte from the command length + emitter.instruction("sub r8, 1"); // consume one byte from the validated sequence width + emitter.instruction("jnz __rt_escapeshellcmd_utf8_copy_x86"); // copy every byte in the valid multibyte sequence + emitter.instruction("mov rax, rsi"); // publish the command cursor after the complete UTF-8 sequence + emitter.instruction("jmp __rt_escapeshellcmd_loop_x86"); // continue with the next independent command sequence + emitter.label("__rt_escapeshellcmd_ascii_x86"); + emitter.instruction("mov dl, BYTE PTR [rax]"); // load the next command byte without relying on NUL termination + emitter.instruction("add rax, 1"); // advance the counted PHP command cursor after consuming the byte + emitter.instruction("sub rcx, 1"); // decrement the number of command bytes still to process + emitter.instruction("test dl, dl"); // would this command byte truncate an OS shell command? + emitter.instruction("jz __rt_escapeshellcmd_nul_x86"); // raise PHP's ValueError for an embedded NUL byte + if windows { + emit_windows_shellcmd_special_checks_x86_64(emitter); + } else { + emitter.instruction("cmp dl, 34"); // is this a double quote needing PHP's POSIX pairing rule? + emitter.instruction("je __rt_escapeshellcmd_quote_x86"); // pair or escape the double quote according to php-src + emitter.instruction("cmp dl, 39"); // is this a single quote needing PHP's POSIX pairing rule? + emitter.instruction("je __rt_escapeshellcmd_quote_x86"); // pair or escape the single quote according to php-src + emit_posix_shellcmd_special_checks_x86_64(emitter); + } + emitter.instruction("mov BYTE PTR [r10], dl"); // copy an ordinary command byte unchanged into the result + emitter.instruction("add r10, 1"); // advance concat storage after the ordinary command byte + emitter.instruction("jmp __rt_escapeshellcmd_loop_x86"); // continue escaping the remaining command text + + if !windows { + emitter.label("__rt_escapeshellcmd_quote_x86"); + emitter.instruction("test r11, r11"); // did the earlier quote record a particular matching endpoint? + emitter.instruction("jz __rt_escapeshellcmd_quote_scan_x86"); // only a first quote begins a new bounded look-ahead scan + emitter.instruction("lea r8, [rax - 1]"); // recover the current quote's exact source address after consuming it + emitter.instruction("cmp r8, r11"); // is this quote the recorded endpoint rather than another quote type? + emitter.instruction("je __rt_escapeshellcmd_quote_paired_x86"); // only the recorded endpoint closes PHP's raw quote pair + emitter.instruction("jmp __rt_escapeshellcmd_quote_unpaired_x86"); // escape an intervening quote without clearing the pending endpoint + emitter.label("__rt_escapeshellcmd_quote_scan_x86"); + emitter.instruction("mov rdi, rax"); // begin a bounded forward scan after the current quote + emitter.instruction("mov r8, rcx"); // preserve the main source-byte count across the quote search + emitter.instruction("mov al, dl"); // make the current quote byte the REPNE SCASB search target + emitter.instruction("repne scasb"); // find the first later matching quote in the counted suffix, if any + emitter.instruction("mov rcx, r8"); // restore the main counted source-byte length after the look-ahead search + emitter.instruction("jne __rt_escapeshellcmd_quote_unpaired_x86"); // escape this quote when no later matching delimiter exists + emitter.instruction("lea r11, [rdi - 1]"); // remember the matching quote's source address for its later iteration + emitter.instruction("mov BYTE PTR [r10], dl"); // preserve the first quote raw because PHP found a matching partner + emitter.instruction("add r10, 1"); // advance concat storage after the first paired quote + emitter.instruction("jmp __rt_escapeshellcmd_loop_x86"); // continue processing command bytes after the paired quote opener + emitter.label("__rt_escapeshellcmd_quote_paired_x86"); + emitter.instruction("xor r11d, r11d"); // clear the pending marker after reaching the matching quote + emitter.instruction("mov BYTE PTR [r10], dl"); // preserve the matching quote raw like PHP's paired-quote case + emitter.instruction("add r10, 1"); // advance concat storage after the paired quote closer + emitter.instruction("jmp __rt_escapeshellcmd_loop_x86"); // continue escaping the remaining command bytes + emitter.label("__rt_escapeshellcmd_quote_unpaired_x86"); + emitter.instruction("mov BYTE PTR [r10], 92"); // prefix an unpaired POSIX quote with a shell backslash + emitter.instruction("add r10, 1"); // advance concat storage after the quote escape prefix + emitter.instruction("mov BYTE PTR [r10], dl"); // copy the original unpaired quote after its escape prefix + emitter.instruction("add r10, 1"); // advance concat storage after the escaped quote + emitter.instruction("jmp __rt_escapeshellcmd_loop_x86"); // continue escaping the remaining command bytes + } + + emitter.label("__rt_escapeshellcmd_escape_x86"); + emitter.instruction(if windows { "mov BYTE PTR [r10], 94" } else { "mov BYTE PTR [r10], 92" }); // choose PHP's Windows caret or POSIX backslash metacharacter prefix + emitter.instruction("add r10, 1"); // advance concat storage after the metacharacter escape prefix + emitter.instruction("mov BYTE PTR [r10], dl"); // write the original metacharacter after its shell escape prefix + emitter.instruction("add r10, 1"); // advance concat storage after the escaped metacharacter + emitter.instruction("jmp __rt_escapeshellcmd_loop_x86"); // continue processing the remaining command bytes + + emitter.label("__rt_escapeshellcmd_done_x86"); + emitter.instruction("mov rdx, r10"); // snapshot the final destination cursor before calculating the command length + emitter.instruction("sub rdx, r9"); // calculate the escaped command length from cursor minus result start + if windows { + emitter.instruction("cmp rdx, 8193"); // php-src rejects escaped Windows commands longer than cmd_max_len plus its trailing NUL + emitter.instruction("ja __rt_escapeshellcmd_output_too_long_x86"); // throw the exact PHP ValueError instead of returning an over-limit command string + } + emitter.instruction("mov rax, r9"); // return either concat-backed or heap-backed command start in the string result register + emitter.instruction("cmp QWORD PTR [rbp - 24], 0"); // determine whether this command result is concat-backed or heap-backed + emitter.instruction("jne __rt_escapeshellcmd_return_x86"); // heap-backed command results must leave concat offset unchanged + abi::emit_load_symbol_to_reg(emitter, "r8", "_concat_off", 0); // reload the previous concat offset before publishing the escaped command + emitter.instruction("add r8, rdx"); // advance concat storage by the escaped command length + abi::emit_store_reg_to_symbol(emitter, "r8", "_concat_off", 0); // publish the new concat offset for later string results + emitter.label("__rt_escapeshellcmd_return_x86"); + emitter.instruction("mov rsp, rbp"); // release local shell-command spill slots before restoring the caller frame + emitter.instruction("pop rbp"); // restore the caller frame pointer before returning the escaped command + emitter.instruction("ret"); // return the escaped command pointer and length pair + + emitter.label("__rt_escapeshellcmd_nul_x86"); + emitter.instruction("cmp QWORD PTR [rbp - 24], 0"); // determine whether the rejected command owns a heap-backed partial result + emitter.instruction("je __rt_escapeshellcmd_nul_release_x86"); // concat-backed partial output has no owned allocation to release + emitter.instruction("mov rax, r9"); // pass the owned partial-result payload to the heap releaser + emitter.instruction("call __rt_heap_free"); // release heap-backed output before the catchable ValueError unwinds + emitter.label("__rt_escapeshellcmd_nul_release_x86"); + emitter.instruction("mov rsp, rbp"); // release the helper frame before tail-entering the catchable exception unwinder + emitter.instruction("pop rbp"); // restore the caller frame pointer before the catchable ValueError tail edge + emit_throw_value_error_x86_64(emitter, "_escapeshellcmd_nul_msg", ESCAPE_SHELL_CMD_NUL_MSG.len()); + if windows { + emitter.label("__rt_escapeshellcmd_input_too_long_x86"); + emitter.instruction("mov rsp, rbp"); // release the helper frame before tail-entering the catchable exception unwinder + emitter.instruction("pop rbp"); // restore the caller frame pointer before the catchable ValueError tail edge + emit_throw_value_error_x86_64( + emitter, + "_escapeshellcmd_input_length_msg", + ESCAPE_SHELL_CMD_INPUT_LENGTH_MSG.len(), + ); + emitter.label("__rt_escapeshellcmd_output_too_long_x86"); + emitter.instruction("cmp QWORD PTR [rbp - 24], 0"); // determine whether the rejected command owns a heap-backed completed result + emitter.instruction("je __rt_escapeshellcmd_output_release_x86"); // concat-backed output has no owned allocation to release + emitter.instruction("mov rax, r9"); // pass the owned completed-result payload to the heap releaser + emitter.instruction("call __rt_heap_free"); // release heap-backed output before the catchable ValueError unwinds + emitter.label("__rt_escapeshellcmd_output_release_x86"); + emitter.instruction("mov rsp, rbp"); // release the helper frame before tail-entering the catchable exception unwinder + emitter.instruction("pop rbp"); // restore the caller frame pointer before the catchable ValueError tail edge + emit_throw_value_error_x86_64( + emitter, + "_escapeshellcmd_output_length_msg", + ESCAPE_SHELL_CMD_OUTPUT_LENGTH_MSG.len(), + ); + } + emitter.label("__rt_escapeshellcmd_capacity_overflow_x86"); + emitter.instruction("mov rsp, rbp"); // release the helper frame before entering the non-returning allocation failure path + emitter.instruction("pop rbp"); // restore the caller frame pointer before the fatal allocation tail edge + emitter.instruction("jmp __rt_heap_exhausted_entry"); // preserve the heap-fatal atom for this cross-helper non-returning tail +} + +/// Emits POSIX `escapeshellcmd()` metacharacter comparisons for one x86_64 byte. +fn emit_posix_shellcmd_special_checks_x86_64(emitter: &mut Emitter) { + for byte in [35, 38, 59, 96, 124, 42, 63, 126, 60, 62, 94, 40, 41, 91, 93, 123, 125, 36, 92, 10] { + emitter.instruction(&format!("cmp dl, {byte}")); // test one PHP POSIX shell metacharacter requiring a backslash prefix + emitter.instruction("je __rt_escapeshellcmd_escape_x86"); // branch when the current byte must be escaped for the POSIX shell + } +} + +/// Emits Windows `escapeshellcmd()` metacharacter comparisons for one x86_64 byte. +fn emit_windows_shellcmd_special_checks_x86_64(emitter: &mut Emitter) { + for byte in [37, 33, 34, 39, 35, 38, 59, 96, 124, 42, 63, 126, 60, 62, 94, 40, 41, 91, 93, 123, 125, 36, 92, 10] { + emitter.instruction(&format!("cmp dl, {byte}")); // test one PHP Windows cmd.exe metacharacter requiring a caret prefix + emitter.instruction("je __rt_escapeshellcmd_escape_x86"); // branch when the current byte must be caret-escaped for cmd.exe + } +} + +/// Emits an AArch64 catchable `ValueError` for an embedded shell-string NUL byte. +fn emit_shell_utf8_sequence_len_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: bounded UTF-8 sequence classifier ---"); + emitter.label_global("__rt_shell_utf8_sequence_len"); + emitter.instruction("test rsi, rsi"); // reject a sequence request with no remaining counted source bytes + emitter.instruction("jz __rt_shell_utf8_invalid_x86"); // an empty suffix has no leading UTF-8 sequence + emitter.instruction("movzx eax, BYTE PTR [rdi]"); // load the candidate leading byte without C-string traversal + emitter.instruction("cmp al, 128"); // is this an ASCII byte including NUL? + emitter.instruction("jb __rt_shell_utf8_one_x86"); // ASCII always consumes exactly one byte + emitter.instruction("cmp al, 194"); // reject continuation bytes and overlong leader bytes + emitter.instruction("jb __rt_shell_utf8_invalid_x86"); // php_mblen skips one invalid leading byte + emitter.instruction("cmp al, 223"); // is this a two-byte leader? + emitter.instruction("jbe __rt_shell_utf8_two_x86"); // validate one continuation byte + emitter.instruction("cmp al, 239"); // is this a three-byte leader? + emitter.instruction("jbe __rt_shell_utf8_three_x86"); // validate two continuation bytes + emitter.instruction("cmp al, 244"); // is this within the Unicode four-byte leader range? + emitter.instruction("ja __rt_shell_utf8_invalid_x86"); // reject impossible UTF-8 leader bytes + emitter.instruction("cmp rsi, 4"); // are all four bytes inside the counted PHP string? + emitter.instruction("jb __rt_shell_utf8_invalid_x86"); // reject a truncated four-byte sequence + emitter.instruction("movzx r8d, BYTE PTR [rdi + 1]"); // retain the first four-byte continuation for Unicode scalar-bound checks + emitter.instruction("mov edx, r8d"); // copy the first continuation before masking its structural bit pattern + emitter.instruction("and edx, 192"); // isolate the continuation-bit pattern + emitter.instruction("cmp edx, 128"); // is the first continuation structurally valid? + emitter.instruction("jne __rt_shell_utf8_invalid_x86"); // reject malformed four-byte sequences + emitter.instruction("cmp al, 240"); // F0 must not encode an overlong four-byte scalar + emitter.instruction("jne __rt_shell_utf8_four_f4_x86"); // only F0 has the lower first-continuation bound + emitter.instruction("cmp r8d, 144"); // must F0's first continuation be at least 0x90? + emitter.instruction("jb __rt_shell_utf8_invalid_x86"); // reject an overlong four-byte scalar + emitter.label("__rt_shell_utf8_four_f4_x86"); + emitter.instruction("cmp al, 244"); // F4 must stay within Unicode's U+10FFFF upper bound + emitter.instruction("jne __rt_shell_utf8_four_rest_x86"); // leaders below F4 have no additional upper continuation bound + emitter.instruction("cmp r8d, 143"); // must F4's first continuation be at most 0x8f? + emitter.instruction("ja __rt_shell_utf8_invalid_x86"); // reject scalar values above U+10FFFF + emitter.label("__rt_shell_utf8_four_rest_x86"); + emitter.instruction("movzx edx, BYTE PTR [rdi + 2]"); // load the second four-byte continuation candidate + emitter.instruction("and edx, 192"); // isolate the continuation-bit pattern + emitter.instruction("cmp edx, 128"); // is the second continuation structurally valid? + emitter.instruction("jne __rt_shell_utf8_invalid_x86"); // reject malformed four-byte sequences + emitter.instruction("movzx edx, BYTE PTR [rdi + 3]"); // load the third four-byte continuation candidate + emitter.instruction("and edx, 192"); // isolate the continuation-bit pattern + emitter.instruction("cmp edx, 128"); // is the third continuation structurally valid? + emitter.instruction("jne __rt_shell_utf8_invalid_x86"); // reject malformed four-byte sequences + emitter.instruction("mov eax, 4"); // report a valid four-byte UTF-8 sequence + emitter.instruction("ret"); // return without clobbering the shell-loop cursor registers + emitter.label("__rt_shell_utf8_three_x86"); + emitter.instruction("cmp rsi, 3"); // are both three-byte continuations inside the source bound? + emitter.instruction("jb __rt_shell_utf8_invalid_x86"); // reject a truncated three-byte sequence + emitter.instruction("movzx r8d, BYTE PTR [rdi + 1]"); // retain the first three-byte continuation for scalar-bound checks + emitter.instruction("mov edx, r8d"); // copy the first continuation before masking its structural bit pattern + emitter.instruction("and edx, 192"); // isolate the continuation-bit pattern + emitter.instruction("cmp edx, 128"); // is the first continuation structurally valid? + emitter.instruction("jne __rt_shell_utf8_invalid_x86"); // reject malformed three-byte sequences + emitter.instruction("cmp al, 224"); // E0 must not encode an overlong three-byte scalar + emitter.instruction("jne __rt_shell_utf8_three_ed_x86"); // only E0 has the lower first-continuation bound + emitter.instruction("cmp r8d, 160"); // must E0's first continuation be at least 0xa0? + emitter.instruction("jb __rt_shell_utf8_invalid_x86"); // reject an overlong three-byte scalar + emitter.label("__rt_shell_utf8_three_ed_x86"); + emitter.instruction("cmp al, 237"); // ED must not encode a UTF-16 surrogate scalar + emitter.instruction("jne __rt_shell_utf8_three_rest_x86"); // non-ED leaders have no additional upper continuation bound + emitter.instruction("cmp r8d, 159"); // must ED's first continuation be at most 0x9f? + emitter.instruction("ja __rt_shell_utf8_invalid_x86"); // reject UTF-16 surrogate scalar values + emitter.label("__rt_shell_utf8_three_rest_x86"); + emitter.instruction("movzx edx, BYTE PTR [rdi + 2]"); // load the second three-byte continuation candidate + emitter.instruction("and edx, 192"); // isolate the continuation-bit pattern + emitter.instruction("cmp edx, 128"); // is the second continuation structurally valid? + emitter.instruction("jne __rt_shell_utf8_invalid_x86"); // reject malformed three-byte sequences + emitter.instruction("mov eax, 3"); // report a valid three-byte UTF-8 sequence + emitter.instruction("ret"); // return without clobbering the shell-loop cursor registers + emitter.label("__rt_shell_utf8_two_x86"); + emitter.instruction("cmp rsi, 2"); // is the two-byte continuation inside the source bound? + emitter.instruction("jb __rt_shell_utf8_invalid_x86"); // reject a truncated two-byte sequence + emitter.instruction("movzx edx, BYTE PTR [rdi + 1]"); // load the two-byte continuation candidate + emitter.instruction("and edx, 192"); // isolate the continuation-bit pattern + emitter.instruction("cmp edx, 128"); // is the continuation structurally valid? + emitter.instruction("jne __rt_shell_utf8_invalid_x86"); // reject malformed two-byte sequences + emitter.instruction("mov eax, 2"); // report a valid two-byte UTF-8 sequence + emitter.instruction("ret"); // return without clobbering the shell-loop cursor registers + emitter.label("__rt_shell_utf8_one_x86"); + emitter.instruction("mov eax, 1"); // report an ASCII sequence for shell-specific classification + emitter.instruction("ret"); // return without clobbering the shell-loop cursor registers + emitter.label("__rt_shell_utf8_invalid_x86"); + emitter.instruction("xor eax, eax"); // request that the caller skip one invalid leading byte + emitter.instruction("ret"); // return without clobbering the shell-loop cursor registers +} + +/// Emits an AArch64 catchable `ValueError` for an embedded shell-string NUL byte. +fn emit_throw_value_error_aarch64(emitter: &mut Emitter, message_symbol: &str, message_len: usize) { + super::super::arrays::value_error::emit_throw_value_error_aarch64(emitter, message_symbol, message_len); +} + +/// Emits an x86_64 catchable `ValueError` for an embedded shell-string NUL byte. +fn emit_throw_value_error_x86_64(emitter: &mut Emitter, message_symbol: &str, message_len: usize) { + super::super::arrays::value_error::emit_throw_value_error_x86_64(emitter, message_symbol, message_len); +} + +#[cfg(test)] +mod tests { + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::*; + + /// Verifies x86_64 Windows emission preflights both shell helpers and has an owned-heap fallback. + #[test] + fn test_windows_x86_shell_escape_emits_capacity_guards() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shell_escapes(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains("__rt_escapeshellarg_heap_x86:\n")); + assert!(asm.contains("__rt_escapeshellcmd_heap_x86:\n")); + assert!(asm.contains("__rt_escapeshellarg_capacity_overflow_x86:\n")); + assert!(asm.contains("__rt_escapeshellcmd_capacity_overflow_x86:\n")); + assert_eq!(asm.matches("jmp __rt_heap_exhausted_entry\n").count(), 2); + assert_eq!(asm.matches("call __rt_heap_exhausted_entry\n").count(), 0); + assert!(asm.matches("cmp r8, 65536\n").count() >= 2); + assert!(asm.matches("call __rt_heap_alloc\n").count() >= 2); + assert_eq!(asm.matches("call __rt_heap_free\n").count(), 4); + assert_eq!(asm.matches("cmp QWORD PTR [rbp - 24], 0\n").count(), 6); + assert!(asm.matches("cmp rdx, 8189\n").count() >= 2); + assert!(asm.matches("cmp rdx, 8193\n").count() >= 2); + assert!(asm.contains("__rt_escapeshellarg_input_too_long_x86:\n")); + assert!(asm.contains("__rt_escapeshellcmd_output_too_long_x86:\n")); + assert!( + asm.contains( + "__rt_escapeshellarg_windows_space_x86:\n mov BYTE PTR [r10], 32\n add r10, 1\n jmp __rt_escapeshellarg_loop_x86\n" + ), + "Windows escapeshellarg must replace %, !, and quotes rather than prefixing them" + ); + + let trailing_backslash_guard = asm + .split("__rt_escapeshellarg_done_x86:\n") + .nth(1) + .and_then(|section| section.split("__rt_escapeshellarg_close_x86:\n").next()) + .expect("Windows escapeshellarg trailing-backslash guard"); + assert_eq!( + trailing_backslash_guard.matches("add r10, 1\n").count(), + 1, + "the duplicated trailing backslash must advance the output cursor exactly once" + ); + } + + /// Verifies both emitted shell helpers route an embedded NUL through the canonical ValueError path. + #[test] + fn test_shell_escape_emits_nul_value_error_paths() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::AArch64)); + emit_shell_escapes(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains("__rt_escapeshellarg_nul:\n")); + assert!(asm.contains("__rt_escapeshellcmd_nul:\n")); + assert!(asm.matches("b __rt_throw_current\n").count() >= 2); + } +} diff --git a/src/codegen_support/runtime/strings/sprintf.rs b/src/codegen_support/runtime/strings/sprintf.rs index 77a2d8c0ff..6c0b9380dd 100644 --- a/src/codegen_support/runtime/strings/sprintf.rs +++ b/src/codegen_support/runtime/strings/sprintf.rs @@ -65,6 +65,8 @@ pub fn emit_sprintf(emitter: &mut Emitter) { emitter.instruction("sub sp, sp, #384"); // allocate stack frame emitter.instruction("stp x29, x30, [sp, #368]"); // save frame pointer and return address emitter.instruction("add x29, sp, #368"); // set frame pointer + emitter.instruction("mov x9, #-1"); // no positional specifier is active yet + emitter.instruction("str x9, [sp, #240]"); // slot freed by the native %s path: saved sequential index // -- save callee-saved registers -- emitter.instruction("stp x19, x20, [sp, #16]"); // save x19, x20 @@ -114,9 +116,60 @@ pub fn emit_sprintf(emitter: &mut Emitter) { // -- scan format specifier into mini buffer at sp+80 -- // Build: '%' + [flags] + [width] + [.precision] + [ll] + type_char + '\0' emitter.label("__rt_sprintf_scan_spec"); + // A positional specifier borrows the argument index without moving it: php's + // sequential counter is untouched, so printf("%s|%2$s|%s", a, b, c) is "a|b|b". + // The previous specifier's borrow is returned here rather than at each of the + // conversion paths' many exits. + emitter.instruction("ldr x9, [sp, #240]"); // did the previous specifier borrow the index? + emitter.instruction("cmn x9, #1"); // -1 means no borrow was in effect + emitter.instruction("b.eq __rt_sprintf_no_borrow"); // nothing to return + emitter.instruction("mov x21, x9"); // give the sequential index back + emitter.instruction("mov x9, #-1"); // clear the borrow marker + emitter.instruction("str x9, [sp, #240]"); // the borrow is settled + emitter.label("__rt_sprintf_no_borrow"); emitter.instruction("add x10, sp, #80"); // mini format buffer start emitter.instruction("mov w15, #37"); // '%' character emitter.instruction("strb w15, [x10], #1"); // write '%' to mini buffer + emitter.instruction("mov x16, #0"); // mini-buffer offset of the precision, 0 until one is scanned + emitter.instruction("mov x9, #-1"); // no custom pad character captured yet + emitter.instruction("str x9, [sp, #248]"); // slot freed by the native %s path + + // -- optional "N$" argument number, a php extension -- + // The digits are ambiguous with a width, so they are only committed once a '$' + // follows; otherwise the cursor rewinds and the width scanner reads them again. + emitter.instruction("mov x13, x19"); // remember the cursor before the digits + emitter.instruction("mov x14, x20"); // remember the remaining format length + emitter.instruction("mov x15, #0"); // accumulated argument number + emitter.label("__rt_sprintf_argnum_scan"); + emitter.instruction("cbz x20, __rt_sprintf_argnum_none"); // format string ended: no argument number + emitter.instruction("ldrb w12, [x19]"); // load the next format byte + emitter.instruction("cmp w12, #48"); // below '0'? + emitter.instruction("b.lt __rt_sprintf_argnum_dollar"); // not a digit: check for the marker + emitter.instruction("cmp w12, #57"); // above '9'? + emitter.instruction("b.gt __rt_sprintf_argnum_dollar"); // not a digit: check for the marker + emitter.instruction("mov x11, #10"); // decimal base + emitter.instruction("mul x15, x15, x11"); // shift the accumulated number one decimal place + emitter.instruction("sub w12, w12, #48"); // digit value + emitter.instruction("add x15, x15, x12"); // accumulate the digit + emitter.instruction("add x19, x19, #1"); // consume the digit + emitter.instruction("sub x20, x20, #1"); // decrement the remaining format length + emitter.instruction("b __rt_sprintf_argnum_scan"); // keep reading digits + emitter.label("__rt_sprintf_argnum_dollar"); + emitter.instruction("cmp x19, x13"); // were there any digits at all? + emitter.instruction("b.eq __rt_sprintf_argnum_none"); // no digits: this is not an argument number + emitter.instruction("cbz x20, __rt_sprintf_argnum_none"); // nothing follows the digits + emitter.instruction("ldrb w12, [x19]"); // load the byte after the digits + emitter.instruction("cmp w12, #36"); // is it the '$' marker? + emitter.instruction("b.ne __rt_sprintf_argnum_none"); // the digits were a width after all + emitter.instruction("cbz x15, __rt_sprintf_argnum_none"); // php numbers arguments from one; 0 is not one + emitter.instruction("add x19, x19, #1"); // consume the '$' + emitter.instruction("sub x20, x20, #1"); // decrement the remaining format length + emitter.instruction("str x21, [sp, #240]"); // borrow the index, remembering where to give it back + emitter.instruction("sub x21, x15, #1"); // php argument numbers are 1-based + emitter.instruction("b __rt_sprintf_scan_flags"); // the specifier continues with its flags + emitter.label("__rt_sprintf_argnum_none"); + emitter.instruction("mov x19, x13"); // rewind so the width scanner re-reads the digits + emitter.instruction("mov x20, x14"); // restore the remaining format length // -- scan flags: '-', '+', '0', ' ', '#' -- emitter.label("__rt_sprintf_scan_flags"); @@ -129,17 +182,44 @@ pub fn emit_sprintf(emitter: &mut Emitter) { emitter.instruction("cmp w12, #48"); // '0' flag? emitter.instruction("b.eq __rt_sprintf_copy_flag"); // yes → copy it emitter.instruction("cmp w12, #32"); // ' ' flag? - emitter.instruction("b.eq __rt_sprintf_copy_flag"); // yes → copy it + emitter.instruction("b.eq __rt_sprintf_drop_flag"); // php accepts the space flag but gives it no meaning emitter.instruction("cmp w12, #35"); // '#' flag? emitter.instruction("b.eq __rt_sprintf_copy_flag"); // yes → copy it + emitter.instruction("cmp w12, #39"); // php's pad-character introducer? + emitter.instruction("b.eq __rt_sprintf_padchar"); // the byte after it is the pad character, whatever it is emitter.instruction("b __rt_sprintf_scan_width"); // no flag → try width + // -- php's "'X": the byte after the quote becomes the pad character, even when + // it is '-' or a digit. libc has no such flag, so it never reaches snprintf. + emitter.label("__rt_sprintf_padchar"); + emitter.instruction("cmp x20, #2"); // is there a byte after the quote? + emitter.instruction("b.lt __rt_sprintf_scan_width"); // malformed: leave it to the width scanner + emitter.instruction("ldrb w12, [x19, #1]"); // load the pad character + emitter.instruction("cmp w12, #48"); // is the pad character '0'? + emitter.instruction("b.ne __rt_sprintf_padchar_custom"); // any other character pads uniformly + emitter.instruction("mov w12, #48"); // '0' is the zero flag: sign-aware, so libc keeps it + emitter.instruction("strb w12, [x10], #1"); // copy it to the mini format as an ordinary flag + emitter.instruction("b __rt_sprintf_padchar_done"); // and let snprintf do the padding + emitter.label("__rt_sprintf_padchar_custom"); + emitter.instruction("str x12, [sp, #248]"); // remember the pad character for the field renderer + emitter.label("__rt_sprintf_padchar_done"); + emitter.instruction("add x19, x19, #2"); // consume the quote and the pad character + emitter.instruction("sub x20, x20, #2"); // decrement the remaining format length + emitter.instruction("b __rt_sprintf_scan_flags"); // keep scanning flags + emitter.label("__rt_sprintf_copy_flag"); emitter.instruction("strb w12, [x10], #1"); // copy flag char to mini buffer emitter.instruction("add x19, x19, #1"); // consume char from format emitter.instruction("sub x20, x20, #1"); // decrement remaining emitter.instruction("b __rt_sprintf_scan_flags"); // check for more flags + // -- flags php parses but ignores: consumed without reaching snprintf, which + // would otherwise reserve a sign column the way C does -- + emitter.label("__rt_sprintf_drop_flag"); + emitter.instruction("add x19, x19, #1"); // consume the flag from the format string + emitter.instruction("sub x20, x20, #1"); // decrement the remaining format length + emitter.instruction("b __rt_sprintf_scan_flags"); // keep scanning flags + // -- scan width: digits -- emitter.label("__rt_sprintf_scan_width"); emitter.instruction("cbz x20, __rt_sprintf_end_spec"); // no chars left @@ -157,6 +237,7 @@ pub fn emit_sprintf(emitter: &mut Emitter) { emitter.label("__rt_sprintf_scan_dot"); emitter.instruction("cmp w12, #46"); // '.' ? emitter.instruction("b.ne __rt_sprintf_scan_type"); // no → must be type char + emitter.instruction("mov x16, x10"); // remember where the precision starts, so integer types can drop it emitter.instruction("strb w12, [x10], #1"); // copy '.' to mini buffer emitter.instruction("add x19, x19, #1"); // consume '.' emitter.instruction("sub x20, x20, #1"); // decrement remaining @@ -186,16 +267,64 @@ pub fn emit_sprintf(emitter: &mut Emitter) { emitter.instruction("b.eq __rt_sprintf_type_float"); // yes → float emitter.instruction("cmp w12, #103"); // 'g' ? emitter.instruction("b.eq __rt_sprintf_type_float"); // yes → float + emitter.instruction("cmp w12, #69"); // 'E' ? + emitter.instruction("b.eq __rt_sprintf_type_float"); // yes → float + emitter.instruction("cmp w12, #71"); // 'G' ? + emitter.instruction("b.eq __rt_sprintf_type_float"); // yes → float emitter.instruction("cmp w12, #115"); // 's' ? emitter.instruction("b.eq __rt_sprintf_type_str"); // yes → string - emitter.instruction("b __rt_sprintf_type_int"); // default → integer + emitter.instruction("cmp w12, #98"); // 'b' ? + emitter.instruction("b.eq __rt_sprintf_type_b"); // yes → php's binary conversion, which C has no equivalent for + emitter.instruction("b __rt_sprintf_type_int_prec"); // default → integer + + // -- integer conversions: php does not give precision the C meaning -- + // %d, %u and %c ignore it outright; %x, %X and %o render nothing at all. Neither + // is "minimum digit count", so the precision never reaches snprintf. + emitter.label("__rt_sprintf_type_int_prec"); + emitter.instruction("cbz x16, __rt_sprintf_type_int"); // no precision was scanned + emitter.instruction("cmp w12, #120"); // is the conversion 'x'? + emitter.instruction("b.eq __rt_sprintf_type_int_blank"); // php renders nothing for a precise hex + emitter.instruction("cmp w12, #88"); // is the conversion 'X'? + emitter.instruction("b.eq __rt_sprintf_type_int_blank"); // php renders nothing for a precise hex + emitter.instruction("cmp w12, #111"); // is the conversion 'o'? + emitter.instruction("b.eq __rt_sprintf_type_int_blank"); // php renders nothing for a precise octal + emitter.instruction("mov x10, x16"); // drop the precision so snprintf never sees it + emitter.instruction("b __rt_sprintf_type_int"); // format the integer without it + + // -- %b: php's binary conversion, which C's formatter does not have -- + // The value is expanded as unsigned 64-bit, so %b of -1 is sixty-four ones, and + // the digits then go through the same field renderer as %s so width, alignment + // and the pad character behave identically. + emitter.label("__rt_sprintf_type_b"); + emitter.instruction("cbnz x16, __rt_sprintf_type_int_blank"); // a precision renders nothing, as it does for %x and %o + emitter.instruction("lsl x15, x21, #4"); // arg offset = index * 16 + emitter.instruction("add x15, x22, x15"); // arg address in caller's stack + emitter.instruction("ldr x1, [x15]"); // load the integer payload + emitter.instruction("add x21, x21, #1"); // increment arg index + emitter.instruction("add x3, sp, #176"); // build backwards from the far end of the scratch buffer + emitter.instruction("mov x4, #0"); // digit count + emitter.label("__rt_sprintf_b_digit"); + emitter.instruction("and x9, x1, #1"); // take the low bit + emitter.instruction("add x9, x9, #48"); // render it as '0' or '1' + emitter.instruction("sub x3, x3, #1"); // move one byte left + emitter.instruction("strb w9, [x3]"); // store the digit + emitter.instruction("add x4, x4, #1"); // count it + emitter.instruction("lsr x1, x1, #1"); // shift the value right, unsigned + emitter.instruction("cbnz x1, __rt_sprintf_b_digit"); // keep going while bits remain + emitter.instruction("b __rt_sprintf_field_render"); // pad and align exactly as %s does + + emitter.label("__rt_sprintf_type_int_blank"); + emitter.instruction("mov x10, x16"); // drop the precision from the scanned prefix + emitter.instruction("add x21, x21, #1"); // php still consumes the argument + emitter.instruction("mov x4, #0"); // nothing to render: the field is pure padding + emitter.instruction("b __rt_sprintf_field_render"); // reuse the string field renderer // -- incomplete specifier at end of format string -- emitter.label("__rt_sprintf_end_spec"); emitter.instruction("b __rt_sprintf_done"); // bail out // ================================================================ - // FLOAT: %f, %e, %g (with optional flags/width/precision) + // FLOAT: %f, %e, %g, %E, %G (with optional flags/width/precision) // Passes the double value on the stack at [sp] for variadic ABI. // ================================================================ emitter.label("__rt_sprintf_type_float"); @@ -222,6 +351,135 @@ pub fn emit_sprintf(emitter: &mut Emitter) { emitter.bl_c("snprintf"); // call libc snprintf // x0 = number of chars written + // snprintf reports the length it *would* have written, which can exceed the + // 128-byte scratch. Copying that many bytes reads past the buffer and emits + // adjacent stack memory, so an oversized result is re-rendered straight into the + // destination, which has the whole 64 KiB concat buffer behind it. + emitter.instruction("cmp x0, #128"); // did the whole result fit in the scratch buffer? + emitter.instruction("b.ge __rt_sprintf_overflow"); // it did not: render it at the destination instead + + // -- PHP parity: %e/%E (and any exponential-form %g/%G) exponent uses the + // -- minimum digit count (no leading zero), but CRT snprintf pads to at + // -- least 2 digits. A double's decimal exponent never exceeds 3 digits and + // -- 3-digit exponents are never zero-padded (they start at magnitude 100), + // -- so the only possible padding is a single leading '0' in a 2-digit + // -- exponent; strip it in place and shrink the byte count by one. + emitter.instruction("add x5, sp, #112"); // scan cursor over the freshly formatted snprintf output + emitter.instruction("mov x6, x0"); // remaining bytes to scan for the 'e'/'E' exponent marker + emitter.label("__rt_sprintf_etrim_scan"); + emitter.instruction("cbz x6, __rt_sprintf_etrim_done"); // no exponent (e.g. %f) -> nothing to trim + emitter.instruction("ldrb w7, [x5]"); // load the next output byte + emitter.instruction("cmp w7, #101"); // is it 'e'? + emitter.instruction("b.eq __rt_sprintf_etrim_found"); // found the exponent marker + emitter.instruction("cmp w7, #69"); // is it 'E'? + emitter.instruction("b.eq __rt_sprintf_etrim_found"); // found the exponent marker + emitter.instruction("add x5, x5, #1"); // advance the scan cursor + emitter.instruction("sub x6, x6, #1"); // decrement the remaining scan length + emitter.instruction("b __rt_sprintf_etrim_scan"); // keep scanning for the exponent marker + emitter.label("__rt_sprintf_etrim_found"); + emitter.instruction("add x5, x5, #1"); // advance past the 'e'/'E' marker + emitter.instruction("sub x6, x6, #1"); // decrement the remaining scan length + emitter.instruction("cbz x6, __rt_sprintf_etrim_done"); // malformed: exponent marker was the last byte -> bail defensively + emitter.instruction("ldrb w7, [x5]"); // load the byte after the exponent marker + emitter.instruction("cmp w7, #43"); // is it '+'? + emitter.instruction("b.eq __rt_sprintf_etrim_sign"); // consume the exponent sign + emitter.instruction("cmp w7, #45"); // is it '-'? + emitter.instruction("b.ne __rt_sprintf_etrim_done"); // C99 always emits an exponent sign; bail defensively if absent + emitter.label("__rt_sprintf_etrim_sign"); + emitter.instruction("add x5, x5, #1"); // advance past the exponent sign + emitter.instruction("sub x6, x6, #1"); // decrement the remaining scan length + emitter.instruction("cmp x6, #2"); // need at least two remaining bytes to test "0" + emitter.instruction("b.lt __rt_sprintf_etrim_done"); // too short to be a padded 2-digit exponent + emitter.instruction("ldrb w7, [x5]"); // load the first exponent digit + emitter.instruction("cmp w7, #48"); // is it '0'? + emitter.instruction("b.ne __rt_sprintf_etrim_done"); // not zero-padded -> nothing to strip + emitter.instruction("ldrb w8, [x5, #1]"); // load the byte after the leading zero + emitter.instruction("cmp w8, #48"); // is it below '0'? + emitter.instruction("b.lt __rt_sprintf_etrim_done"); // not a digit -> the '0' was the only exponent digit, keep it + emitter.instruction("cmp w8, #57"); // is it above '9'? + emitter.instruction("b.gt __rt_sprintf_etrim_done"); // not a digit -> keep the only exponent digit + // -- guard: a right-justified WIDTH field pads BEFORE the sign/mantissa with + // -- ' ' or '0'. Stripping a byte from the exponent would shrink the total + // -- field width, so detect that padding and skip the strip entirely rather + // -- than corrupt the requested width (a documented, bounded residual gap; + // -- the no-width and left-justified-width cases below are fully handled). + emitter.instruction("add x9, sp, #112"); // buffer start + emitter.instruction("ldrb w12, [x9]"); // first output byte + emitter.instruction("cmp w12, #32"); // is it a space (space-padded field)? + emitter.instruction("b.eq __rt_sprintf_etrim_done"); // space padding present -> skip the strip + emitter.instruction("mov x14, x9"); // cursor for the (optional) leading '-'/'+' sign + emitter.instruction("cmp w12, #45"); // is the first byte a '-' sign? + emitter.instruction("b.eq __rt_sprintf_etrim_lead_sign"); // yes -> skip past it before checking for zero-padding + emitter.instruction("cmp w12, #43"); // is the first byte a '+' sign (the '+' flag)? + emitter.instruction("b.ne __rt_sprintf_etrim_lead_check"); // no sign -> check directly + emitter.label("__rt_sprintf_etrim_lead_sign"); + emitter.instruction("add x14, x14, #1"); // skip past the sign before checking for zero-padding + emitter.label("__rt_sprintf_etrim_lead_check"); + emitter.instruction("ldrb w12, [x14]"); // byte after the optional sign + emitter.instruction("cmp w12, #48"); // is it '0'? + emitter.instruction("b.ne __rt_sprintf_etrim_shift_setup"); // not zero -> no zero-padding, safe to strip + emitter.instruction("ldrb w12, [x14, #1]"); // byte after that leading zero + emitter.instruction("cmp w12, #46"); // is it '.' (the leading zero IS the legitimate mantissa digit)? + emitter.instruction("b.eq __rt_sprintf_etrim_shift_setup"); // legitimate "0.xxx" mantissa -> safe to strip the exponent + emitter.instruction("b __rt_sprintf_etrim_done"); // zero-padded width field -> skip the strip + // -- confirmed a padded 2-digit exponent ("0" + digit) with no right-justify padding: shift the tail left by one byte, dropping the leading zero -- + emitter.label("__rt_sprintf_etrim_shift_setup"); + emitter.instruction("add x9, x9, x0"); // x9 = original buffer end (using the pre-trim snprintf byte count) + emitter.instruction("ldrb w12, [x9, #-1]"); // last output byte (before this trim) + emitter.instruction("cmp w12, #32"); // was the field left-justify space-padded? + emitter.instruction("cset x13, eq"); // x13 = 1 when trailing-space padding is present + emitter.instruction("add x11, x5, #1"); // source cursor = byte after the leading zero + emitter.instruction("mov x9, x5"); // dest cursor = the leading zero's position + emitter.instruction("add x8, sp, #112"); // recompute the scratch buffer base + emitter.instruction("add x8, x8, x0"); // x8 = original buffer end (fixed, independent of the shift cursors) + emitter.label("__rt_sprintf_etrim_shift"); + emitter.instruction("cmp x11, x8"); // reached the end of the original output? + emitter.instruction("b.ge __rt_sprintf_etrim_shift_done"); // shift complete + emitter.instruction("ldrb w14, [x11], #1"); // load the next byte to shift down + emitter.instruction("strb w14, [x9], #1"); // shift it left by one position + emitter.instruction("b __rt_sprintf_etrim_shift"); // continue shifting + emitter.label("__rt_sprintf_etrim_shift_done"); + emitter.instruction("cbz x13, __rt_sprintf_etrim_shrink"); // no trailing padding -> just shrink the byte count + emitter.instruction("mov w12, #32"); // ASCII space + emitter.instruction("strb w12, [x9]"); // restore the requested field width with one more trailing pad space + emitter.instruction("b __rt_sprintf_etrim_done"); // keep x0 unchanged: total field width is preserved + emitter.label("__rt_sprintf_etrim_shrink"); + emitter.instruction("sub x0, x0, #1"); // no padding to preserve -> one byte shorter after dropping the leading exponent zero + emitter.label("__rt_sprintf_etrim_done"); + + + // -- php's "'X" pad character on a conversion libc rendered -- + // libc has no such flag, so snprintf padded with spaces. A numeric conversion + // never contains a space of its own once the space flag is dropped, so the + // padding is exactly the run of spaces at one end or the other. + emitter.instruction("ldr x9, [sp, #248]"); // did the specifier name a pad character? + emitter.instruction("cmn x9, #1"); // -1 means it did not + emitter.instruction("b.eq __rt_sprintf_pad_done_f"); // leave the spaces alone + emitter.instruction("add x5, sp, #112"); // scan the freshly rendered output + emitter.instruction("mov x6, x0"); // bytes rendered + emitter.label("__rt_sprintf_pad_lead_f"); + emitter.instruction("cbz x6, __rt_sprintf_pad_done_f"); // the whole field was padding + emitter.instruction("ldrb w11, [x5]"); // load the next byte + emitter.instruction("cmp w11, #32"); // is it a padding space? + emitter.instruction("b.ne __rt_sprintf_pad_trail_f"); // the value starts here: try the other end + emitter.instruction("strb w9, [x5]"); // substitute the pad character + emitter.instruction("add x5, x5, #1"); // advance + emitter.instruction("sub x6, x6, #1"); // one fewer byte to inspect + emitter.instruction("b __rt_sprintf_pad_lead_f"); // keep substituting + emitter.label("__rt_sprintf_pad_trail_f"); + emitter.instruction("add x5, sp, #112"); // back to the start of the output + emitter.instruction("add x5, x5, x0"); // one past its end + emitter.instruction("mov x7, x0"); // bytes still to inspect + emitter.label("__rt_sprintf_pad_trail_loop_f"); + emitter.instruction("cbz x7, __rt_sprintf_pad_done_f"); // nothing left + emitter.instruction("sub x5, x5, #1"); // step back one byte + emitter.instruction("ldrb w11, [x5]"); // load it + emitter.instruction("cmp w11, #32"); // is it a padding space? + emitter.instruction("b.ne __rt_sprintf_pad_done_f"); // the value ends here + emitter.instruction("strb w9, [x5]"); // substitute the pad character + emitter.instruction("sub x7, x7, #1"); // one fewer byte to inspect + emitter.instruction("b __rt_sprintf_pad_trail_loop_f"); // keep substituting + emitter.label("__rt_sprintf_pad_done_f"); // -- copy snprintf result to concat_buf -- emitter.instruction("mov x4, x0"); // chars to copy emitter.instruction("add x3, sp, #112"); // source buffer @@ -272,6 +530,46 @@ pub fn emit_sprintf(emitter: &mut Emitter) { emitter.bl_c("snprintf"); // call libc snprintf // x0 = number of chars written + // snprintf reports the length it *would* have written, which can exceed the + // 128-byte scratch. Copying that many bytes reads past the buffer and emits + // adjacent stack memory, so an oversized result is re-rendered straight into the + // destination, which has the whole 64 KiB concat buffer behind it. + emitter.instruction("cmp x0, #128"); // did the whole result fit in the scratch buffer? + emitter.instruction("b.ge __rt_sprintf_overflow"); // it did not: render it at the destination instead + + + // -- php's "'X" pad character on a conversion libc rendered -- + // libc has no such flag, so snprintf padded with spaces. A numeric conversion + // never contains a space of its own once the space flag is dropped, so the + // padding is exactly the run of spaces at one end or the other. + emitter.instruction("ldr x9, [sp, #248]"); // did the specifier name a pad character? + emitter.instruction("cmn x9, #1"); // -1 means it did not + emitter.instruction("b.eq __rt_sprintf_pad_done_i"); // leave the spaces alone + emitter.instruction("add x5, sp, #112"); // scan the freshly rendered output + emitter.instruction("mov x6, x0"); // bytes rendered + emitter.label("__rt_sprintf_pad_lead_i"); + emitter.instruction("cbz x6, __rt_sprintf_pad_done_i"); // the whole field was padding + emitter.instruction("ldrb w11, [x5]"); // load the next byte + emitter.instruction("cmp w11, #32"); // is it a padding space? + emitter.instruction("b.ne __rt_sprintf_pad_trail_i"); // the value starts here: try the other end + emitter.instruction("strb w9, [x5]"); // substitute the pad character + emitter.instruction("add x5, x5, #1"); // advance + emitter.instruction("sub x6, x6, #1"); // one fewer byte to inspect + emitter.instruction("b __rt_sprintf_pad_lead_i"); // keep substituting + emitter.label("__rt_sprintf_pad_trail_i"); + emitter.instruction("add x5, sp, #112"); // back to the start of the output + emitter.instruction("add x5, x5, x0"); // one past its end + emitter.instruction("mov x7, x0"); // bytes still to inspect + emitter.label("__rt_sprintf_pad_trail_loop_i"); + emitter.instruction("cbz x7, __rt_sprintf_pad_done_i"); // nothing left + emitter.instruction("sub x5, x5, #1"); // step back one byte + emitter.instruction("ldrb w11, [x5]"); // load it + emitter.instruction("cmp w11, #32"); // is it a padding space? + emitter.instruction("b.ne __rt_sprintf_pad_done_i"); // the value ends here + emitter.instruction("strb w9, [x5]"); // substitute the pad character + emitter.instruction("sub x7, x7, #1"); // one fewer byte to inspect + emitter.instruction("b __rt_sprintf_pad_trail_loop_i"); // keep substituting + emitter.label("__rt_sprintf_pad_done_i"); // -- copy snprintf result to concat_buf -- emitter.instruction("mov x4, x0"); // chars to copy emitter.instruction("add x3, sp, #112"); // source buffer @@ -292,9 +590,13 @@ pub fn emit_sprintf(emitter: &mut Emitter) { // so we copy the string to a temp buffer at sp+240 and null-terminate it. // The variadic pointer goes on the stack at [sp]. // ================================================================ + // php's %s is only "truncate to precision, then pad to width with the pad + // character", so it needs no C formatter. Routing it through snprintf required a + // NUL-terminated copy in a 128-byte buffer, which silently truncated every + // argument at 127 bytes and cut the string short at any NUL byte -- php strings + // are binary and may contain them. The bytes now go straight to the destination + // using the stored length: binary-safe, and with no length ceiling. emitter.label("__rt_sprintf_type_str"); - emitter.instruction("strb w12, [x10], #1"); // copy 's' to mini buffer - emitter.instruction("strb wzr, [x10]"); // null-terminate format string // -- load next arg (string: ptr + tag|len) -- emitter.instruction("lsl x15, x21, #4"); // arg offset = index * 16 @@ -304,50 +606,136 @@ pub fn emit_sprintf(emitter: &mut Emitter) { emitter.instruction("lsr x4, x4, #8"); // extract length (shift right 8) emitter.instruction("add x21, x21, #1"); // increment arg index - // -- copy string to temp buffer at sp+240 and null-terminate -- - // Limit copy to 127 bytes to fit in our 128-byte buffer - emitter.instruction("cmp x4, #127"); // string longer than buffer? - emitter.instruction("b.le __rt_sprintf_str_len_ok"); // no → use actual length - emitter.instruction("mov x4, #127"); // clamp to 127 bytes - - emitter.label("__rt_sprintf_str_len_ok"); - emitter.instruction("add x6, sp, #240"); // temp buffer for null-terminated copy - emitter.instruction("mov x7, x4"); // bytes to copy - - emitter.label("__rt_sprintf_strcopy"); - emitter.instruction("cbz x7, __rt_sprintf_strcopy_done"); // done copying - emitter.instruction("ldrb w15, [x3], #1"); // load source byte - emitter.instruction("strb w15, [x6], #1"); // write to temp buffer - emitter.instruction("sub x7, x7, #1"); // decrement counter - emitter.instruction("b __rt_sprintf_strcopy"); // continue copying - - emitter.label("__rt_sprintf_strcopy_done"); - emitter.instruction("strb wzr, [x6]"); // null-terminate the copy - - // -- store variadic arg (pointer to null-terminated copy) on stack -- - emitter.instruction("add x3, sp, #240"); // pointer to null-terminated string - emitter.instruction("str x3, [sp]"); // variadic string ptr at [sp] - - // -- call snprintf(buf, 128, fmt) with variadic string ptr on stack -- - emitter.instruction("add x0, sp, #112"); // output buffer at sp+112 - emitter.instruction("mov x1, #128"); // buffer size - emitter.instruction("add x2, sp, #80"); // mini format string at sp+80 - emitter.bl_c("snprintf"); // call libc snprintf - // x0 = number of chars written - - // -- copy snprintf result to concat_buf -- - emitter.instruction("mov x4, x0"); // chars to copy - emitter.instruction("add x3, sp, #112"); // source buffer - - emitter.label("__rt_sprintf_copy_s"); - emitter.instruction("cbz x4, __rt_sprintf_copy_s_done"); // no bytes left - emitter.instruction("ldrb w15, [x3], #1"); // load byte from snprintf output - emitter.instruction("strb w15, [x23], #1"); // write to concat_buf - emitter.instruction("sub x4, x4, #1"); // decrement counter - emitter.instruction("b __rt_sprintf_copy_s"); // continue copying + // -- re-read the scanned prefix (sp+80 .. x10) for flags, width and precision -- + emitter.label("__rt_sprintf_field_render"); + emitter.instruction("add x5, sp, #81"); // first byte after the '%' + emitter.instruction("mov x6, #0"); // left-align flag, clear by default + emitter.instruction("mov x7, #32"); // pad character, space by default + emitter.instruction("ldr x9, [sp, #248]"); // did the specifier name its own pad character? + emitter.instruction("cmn x9, #1"); // -1 means it did not + emitter.instruction("csel x7, x9, x7, ne"); // php's "'X" overrides the default + emitter.instruction("mov x9, #0"); // field width + emitter.instruction("mov x11, #-1"); // precision, -1 when absent + emitter.label("__rt_sprintf_str_flag"); + emitter.instruction("cmp x5, x10"); // consumed the whole scanned prefix? + emitter.instruction("b.ge __rt_sprintf_str_field"); // nothing left: render the field + emitter.instruction("ldrb w12, [x5]"); // load the next prefix byte + emitter.instruction("cmp w12, #45"); // is it '-'? + emitter.instruction("b.ne __rt_sprintf_str_flag_zero"); // not the left-align flag + emitter.instruction("mov x6, #1"); // left-align the field + emitter.instruction("add x5, x5, #1"); // consume the flag + emitter.instruction("b __rt_sprintf_str_flag"); // keep reading flags + emitter.label("__rt_sprintf_str_flag_zero"); + emitter.instruction("cmp w12, #48"); // is it '0'? + emitter.instruction("b.ne __rt_sprintf_str_flag_skip"); // not the zero-pad flag + emitter.instruction("mov x7, #48"); // pad with '0' instead of space + emitter.instruction("add x5, x5, #1"); // consume the flag + emitter.instruction("b __rt_sprintf_str_flag"); // keep reading flags + emitter.label("__rt_sprintf_str_flag_skip"); + emitter.instruction("cmp w12, #43"); // is it '+'? + emitter.instruction("b.eq __rt_sprintf_str_flag_drop"); // php ignores '+' on strings + emitter.instruction("cmp w12, #32"); // is it ' '? + emitter.instruction("b.ne __rt_sprintf_str_width"); // no flags left: read the width + emitter.label("__rt_sprintf_str_flag_drop"); + emitter.instruction("add x5, x5, #1"); // consume the ignored flag + emitter.instruction("b __rt_sprintf_str_flag"); // keep reading flags + emitter.label("__rt_sprintf_str_width"); + emitter.instruction("cmp x5, x10"); // consumed the whole scanned prefix? + emitter.instruction("b.ge __rt_sprintf_str_field"); // nothing left: render the field + emitter.instruction("ldrb w12, [x5]"); // load the next prefix byte + emitter.instruction("cmp w12, #46"); // is it the precision '.'? + emitter.instruction("b.eq __rt_sprintf_str_prec_start"); // switch to reading the precision + emitter.instruction("cmp w12, #48"); // below '0'? + emitter.instruction("b.lt __rt_sprintf_str_field"); // not a digit: the width is complete + emitter.instruction("cmp w12, #57"); // above '9'? + emitter.instruction("b.gt __rt_sprintf_str_field"); // not a digit: the width is complete + emitter.instruction("mov x13, #10"); // decimal base + emitter.instruction("mul x9, x9, x13"); // shift the accumulated width one decimal place + emitter.instruction("sub w12, w12, #48"); // digit value + emitter.instruction("add x9, x9, x12"); // accumulate the width digit + emitter.instruction("add x5, x5, #1"); // consume the digit + emitter.instruction("b __rt_sprintf_str_width"); // keep reading width digits + emitter.label("__rt_sprintf_str_prec_start"); + emitter.instruction("add x5, x5, #1"); // consume the '.' + emitter.instruction("mov x11, #0"); // an explicit precision starts at zero + emitter.label("__rt_sprintf_str_prec"); + emitter.instruction("cmp x5, x10"); // consumed the whole scanned prefix? + emitter.instruction("b.ge __rt_sprintf_str_field"); // nothing left: render the field + emitter.instruction("ldrb w12, [x5]"); // load the next prefix byte + emitter.instruction("cmp w12, #48"); // below '0'? + emitter.instruction("b.lt __rt_sprintf_str_field"); // not a digit: the precision is complete + emitter.instruction("cmp w12, #57"); // above '9'? + emitter.instruction("b.gt __rt_sprintf_str_field"); // not a digit: the precision is complete + emitter.instruction("mov x13, #10"); // decimal base + emitter.instruction("mul x11, x11, x13"); // shift the accumulated precision one decimal place + emitter.instruction("sub w12, w12, #48"); // digit value + emitter.instruction("add x11, x11, x12"); // accumulate the precision digit + emitter.instruction("add x5, x5, #1"); // consume the digit + emitter.instruction("b __rt_sprintf_str_prec"); // keep reading precision digits + emitter.label("__rt_sprintf_str_field"); + emitter.instruction("cmn x11, #1"); // was a precision given? + emitter.instruction("b.eq __rt_sprintf_str_pad_count"); // no precision: keep the whole string + emitter.instruction("cmp x4, x11"); // is the string longer than the precision? + emitter.instruction("csel x4, x11, x4, gt"); // php truncates the string to the precision + emitter.label("__rt_sprintf_str_pad_count"); + emitter.instruction("sub x13, x9, x4"); // padding = width - rendered length + emitter.instruction("cmp x13, #0"); // is the field already at least as wide? + emitter.instruction("csel x13, xzr, x13, lt"); // never pad a negative amount + emitter.instruction("cbnz x6, __rt_sprintf_str_copy_first"); // left-align writes the bytes first + emitter.label("__rt_sprintf_str_pad_lead"); + emitter.instruction("cbz x13, __rt_sprintf_str_copy_tail"); // leading padding written + emitter.instruction("strb w7, [x23], #1"); // write one pad character + emitter.instruction("sub x13, x13, #1"); // one fewer pad character to write + emitter.instruction("b __rt_sprintf_str_pad_lead"); // keep padding + emitter.label("__rt_sprintf_str_copy_tail"); + emitter.instruction("cbz x4, __rt_sprintf_loop"); // the whole string is written + emitter.instruction("ldrb w12, [x3], #1"); // load one argument byte + emitter.instruction("strb w12, [x23], #1"); // append it to the destination + emitter.instruction("sub x4, x4, #1"); // one fewer byte to copy + emitter.instruction("b __rt_sprintf_str_copy_tail"); // keep copying + emitter.label("__rt_sprintf_str_copy_first"); + emitter.instruction("cbz x4, __rt_sprintf_str_pad_trail"); // the whole string is written + emitter.instruction("ldrb w12, [x3], #1"); // load one argument byte + emitter.instruction("strb w12, [x23], #1"); // append it to the destination + emitter.instruction("sub x4, x4, #1"); // one fewer byte to copy + emitter.instruction("b __rt_sprintf_str_copy_first"); // keep copying + emitter.label("__rt_sprintf_str_pad_trail"); + emitter.instruction("cbz x13, __rt_sprintf_loop"); // trailing padding written + emitter.instruction("strb w7, [x23], #1"); // write one pad character + emitter.instruction("sub x13, x13, #1"); // one fewer pad character to write + emitter.instruction("b __rt_sprintf_str_pad_trail"); // keep padding - emitter.label("__rt_sprintf_copy_s_done"); - emitter.instruction("b __rt_sprintf_loop"); // next format char + // ================================================================ + // OVERFLOW: the conversion did not fit the scratch buffer + // The variadic argument is still staged at [sp], so the same mini format can be + // rendered a second time, this time writing at the destination cursor with the + // concat buffer's remaining capacity as the bound. + // ================================================================ + emitter.label("__rt_sprintf_overflow"); + emitter.instruction("mov x0, x23"); // write at the destination cursor + crate::codegen_support::abi::emit_symbol_address(emitter, "x9", "_concat_buf"); + emitter.instruction("sub x11, x23, x9"); // bytes of the concat buffer already used + emitter.instruction("mov x9, #65536"); // total concat buffer size + emitter.instruction("sub x1, x9, x11"); // remaining capacity, including the terminator + emitter.instruction("add x2, sp, #80"); // the same one-specifier mini format string + + // The first call clobbered the variadic argument registers, which are + // caller-saved. Apple's ABI passes every variadic argument on the stack, so the + // copy at [sp] alone carried the re-render there and this omission was invisible + // on macOS; AAPCS64 passes the first one in x3 (or d0 for a double), so on Linux + // the second call formatted whatever the first had left behind -- right length, + // wrong bytes. The stack copy is still intact, so it is the restore source. + emitter.instruction("ldr x3, [sp]"); // reload the variadic payload for the integer register + if emitter.platform == Platform::Linux { + emitter.instruction("fmov d0, x3"); // and for the FP register, which AAPCS64 uses for a double + } + emitter.bl_c("snprintf"); // re-render at the destination + emitter.instruction("cmp x0, x1"); // did the second render fill the remaining capacity? + emitter.instruction("b.lt __rt_sprintf_overflow_done"); // it fit: advance by what was written + emitter.instruction("sub x0, x1, #1"); // clamp to what actually landed, excluding the terminator + emitter.label("__rt_sprintf_overflow_done"); + emitter.instruction("add x23, x23, x0"); // advance the destination cursor past the rendered value + emitter.instruction("b __rt_sprintf_loop"); // continue scanning the format string // ================================================================ // DONE: finalize result and clean up @@ -375,3 +763,90 @@ pub fn emit_sprintf(emitter: &mut Emitter) { emitter.instruction("add sp, sp, x0"); // pop caller's args from stack emitter.instruction("ret"); // return } + +#[cfg(test)] +mod tests { + use crate::codegen_support::platform::{Arch, Target}; + + use super::*; + + /// Verifies the `%E`/`%G` uppercase specifiers dispatch to the float path + /// alongside `%f`/`%e`/`%g` on AArch64 (a WF10b fix: they previously fell + /// through to the integer path, reinterpreting the double's raw bits as an + /// integer and producing garbage). + #[test] + fn test_emit_sprintf_aarch64_dispatches_uppercase_e_and_g_to_float_path() { + let mut emitter = Emitter::new(Target::new(Platform::MacOS, Arch::AArch64)); + emit_sprintf(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains("cmp w12, #69\n"), "'E' (69) must be checked"); + assert!(asm.contains("cmp w12, #71\n"), "'G' (71) must be checked"); + } + + /// Verifies the AArch64 `%e`/`%E` exponent-trim (PHP's minimum-digit + /// exponent) and its right-justified-width padding guard are present, + /// mirroring the x86_64 fix. + #[test] + fn test_emit_sprintf_aarch64_float_path_has_exponent_trim_with_padding_guard() { + let mut emitter = Emitter::new(Target::new(Platform::MacOS, Arch::AArch64)); + emit_sprintf(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains("__rt_sprintf_etrim_scan\n")); + assert!(asm.contains("__rt_sprintf_etrim_shift_setup\n")); + assert!( + asm.contains("cmp w12, #32\n"), + "must detect space-padded (right-justified) fields to guard the strip" + ); + } + + /// Verifies the oversized re-render restores the variadic argument first. + /// + /// `snprintf` reports the length it *would* have written, so a result wider than + /// the 128-byte scratch is rendered a second time straight into the destination. + /// The argument registers are caller-saved and the first call clobbers them, so + /// the second call has to reload the payload from the stack copy. + /// + /// Apple's ABI passes every variadic argument on the stack, which makes the + /// stack copy sufficient on macOS and hides an omission here entirely. AAPCS64 + /// passes the first one in `x3`, or `d0` when the conversion names a double, so + /// only Linux shows the defect -- as wrong bytes at the right length, which a + /// length comparison cannot see. Hence a check on the emitted text. + #[test] + fn test_emit_sprintf_overflow_restores_variadic_argument() { + for (platform, wants_fp_register) in + [(Platform::Linux, true), (Platform::MacOS, false)] + { + let mut emitter = Emitter::new(Target::new(platform, Arch::AArch64)); + emit_sprintf(&mut emitter); + let asm = emitter.output(); + + let start = asm + .find("__rt_sprintf_overflow:\n") + .expect("overflow label missing"); + let end = asm[start..] + .find("__rt_sprintf_overflow_done:\n") + .map(|offset| start + offset) + .expect("overflow-done label missing after the overflow section"); + let section = &asm[start..end]; + + assert!( + section.contains("ldr x3, [sp]\n"), + "{:?}: the re-render must reload the variadic payload", + platform + ); + assert_eq!( + section.contains("fmov d0, x3\n"), + wants_fp_register, + "{:?}: the FP register is AAPCS64's variadic double slot only", + platform + ); + assert!( + section.find("ldr x3, [sp]\n") < section.find("bl _snprintf").or(section.find("bl snprintf")), + "{:?}: the reload must precede the re-render call", + platform + ); + } + } +} diff --git a/src/codegen_support/runtime/strings/sprintf_x86_64.rs b/src/codegen_support/runtime/strings/sprintf_x86_64.rs index 0fb6165e50..7b95d40bcc 100644 --- a/src/codegen_support/runtime/strings/sprintf_x86_64.rs +++ b/src/codegen_support/runtime/strings/sprintf_x86_64.rs @@ -9,6 +9,7 @@ //! - Formatting helpers parse format strings and marshal values through target ABI calls or emitted formatting paths. use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Platform; /// Emits the `__rt_sprintf` runtime helper for Linux x86_64. /// @@ -53,6 +54,7 @@ pub(super) fn emit_sprintf_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("xor r14d, r14d"); // start consuming packed variadic argument records from logical index zero emitter.instruction("lea r15, [rbp + 16]"); // point at the caller-owned packed variadic argument records that begin above the saved return address emitter.instruction("mov QWORD PTR [rbp - 64], rdi"); // preserve the packed variadic argument count so the helper can discard the caller records before returning + emitter.instruction("mov QWORD PTR [rbp - 248], -1"); // no positional specifier is active yet crate::codegen_support::abi::emit_symbol_address(emitter, "r10", "_concat_off"); emitter.instruction("mov r11, QWORD PTR [r10]"); // load the current concat-buffer write cursor before appending the formatted output crate::codegen_support::abi::emit_symbol_address(emitter, "rcx", "_concat_buf"); @@ -88,6 +90,58 @@ pub(super) fn emit_sprintf_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("lea r10, [rbp - 96]"); // point at the mini format-string buffer used for one-specifier snprintf calls emitter.instruction("mov BYTE PTR [r10], 37"); // seed the mini format-string buffer with the leading '%' introducer emitter.instruction("add r10, 1"); // advance the mini format-string cursor after writing the leading '%' introducer + emitter.instruction("mov QWORD PTR [rbp - 232], 0"); // mini-buffer address of the precision, zero until one is scanned + emitter.instruction("mov QWORD PTR [rbp - 264], -1"); // no custom pad character captured yet + + // A positional specifier borrows the argument index without moving it: php's + // sequential counter is untouched, so printf("%s|%2$s|%s", a, b, c) is "a|b|b". + // The previous specifier's borrow is returned here rather than at each of the + // conversion paths' many exits. + emitter.instruction("cmp QWORD PTR [rbp - 248], -1"); // did the previous specifier borrow the index? + emitter.instruction("je __rt_sprintf_no_borrow_linux_x86_64"); // nothing to return + emitter.instruction("mov r14, QWORD PTR [rbp - 248]"); // give the sequential index back + emitter.instruction("mov QWORD PTR [rbp - 248], -1"); // the borrow is settled + emitter.label("__rt_sprintf_no_borrow_linux_x86_64"); + + // -- optional "N$" argument number, a php extension -- + // The digits are ambiguous with a width, so they are only committed once a '$' + // follows; otherwise the cursor rewinds and the width scanner reads them again. + emitter.instruction("mov r11, r12"); // remember the cursor before the digits + emitter.instruction("mov QWORD PTR [rbp - 256], r13"); // remember the remaining format length + emitter.instruction("xor r9d, r9d"); // accumulated argument number + emitter.label("__rt_sprintf_argnum_scan_linux_x86_64"); + emitter.instruction("test r13, r13"); // format string ended: no argument number + emitter.instruction("jz __rt_sprintf_argnum_none_linux_x86_64"); // nothing left to read + emitter.instruction("movzx r8d, BYTE PTR [r12]"); // load the next format byte + emitter.instruction("cmp r8b, 48"); // below '0'? + emitter.instruction("jb __rt_sprintf_argnum_dollar_linux_x86_64"); // not a digit: check for the marker + emitter.instruction("cmp r8b, 57"); // above '9'? + emitter.instruction("ja __rt_sprintf_argnum_dollar_linux_x86_64"); // not a digit: check for the marker + emitter.instruction("imul r9, r9, 10"); // shift the accumulated number one decimal place + emitter.instruction("sub r8d, 48"); // digit value + emitter.instruction("add r9, r8"); // accumulate the digit + emitter.instruction("add r12, 1"); // consume the digit + emitter.instruction("sub r13, 1"); // decrement the remaining format length + emitter.instruction("jmp __rt_sprintf_argnum_scan_linux_x86_64"); // keep reading digits + emitter.label("__rt_sprintf_argnum_dollar_linux_x86_64"); + emitter.instruction("cmp r12, r11"); // were there any digits at all? + emitter.instruction("je __rt_sprintf_argnum_none_linux_x86_64"); // no digits: this is not an argument number + emitter.instruction("test r13, r13"); // anything after the digits? + emitter.instruction("jz __rt_sprintf_argnum_none_linux_x86_64"); // nothing follows them + emitter.instruction("movzx r8d, BYTE PTR [r12]"); // load the byte after the digits + emitter.instruction("cmp r8b, 36"); // is it the '$' marker? + emitter.instruction("jne __rt_sprintf_argnum_none_linux_x86_64"); // the digits were a width after all + emitter.instruction("test r9, r9"); // php numbers arguments from one + emitter.instruction("jz __rt_sprintf_argnum_none_linux_x86_64"); // position zero is not one + emitter.instruction("add r12, 1"); // consume the '$' + emitter.instruction("sub r13, 1"); // decrement the remaining format length + emitter.instruction("mov QWORD PTR [rbp - 248], r14"); // borrow the index, remembering where to give it back + emitter.instruction("sub r9, 1"); // php argument numbers are 1-based + emitter.instruction("mov r14, r9"); // select the named argument for this specifier + emitter.instruction("jmp __rt_sprintf_scan_flags_linux_x86_64"); // the specifier continues with its flags + emitter.label("__rt_sprintf_argnum_none_linux_x86_64"); + emitter.instruction("mov r12, r11"); // rewind so the width scanner re-reads the digits + emitter.instruction("mov r13, QWORD PTR [rbp - 256]"); // restore the remaining format length emitter.label("__rt_sprintf_scan_flags_linux_x86_64"); emitter.instruction("test r13, r13"); // are there any format bytes left to inspect for flag characters? @@ -100,11 +154,31 @@ pub(super) fn emit_sprintf_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("cmp r8b, 48"); // is the next format byte the zero-pad '0' flag? emitter.instruction("je __rt_sprintf_copy_flag_linux_x86_64"); // copy the current flag byte into the mini format string when it is '0' emitter.instruction("cmp r8b, 32"); // is the next format byte the space-sign flag? - emitter.instruction("je __rt_sprintf_copy_flag_linux_x86_64"); // copy the current flag byte into the mini format string when it is a space + emitter.instruction("je __rt_sprintf_drop_flag_linux_x86_64"); // php accepts the space flag but gives it no meaning emitter.instruction("cmp r8b, 35"); // is the next format byte the alternate-form '#' flag? emitter.instruction("je __rt_sprintf_copy_flag_linux_x86_64"); // copy the current flag byte into the mini format string when it is '#' + emitter.instruction("cmp r8b, 39"); // php's pad-character introducer? + emitter.instruction("je __rt_sprintf_padchar_linux_x86_64"); // the byte after it is the pad character, whatever it is emitter.instruction("jmp __rt_sprintf_scan_width_linux_x86_64"); // move on to width parsing once no more flag characters remain + // -- php's "'X": the byte after the quote becomes the pad character, even when + // it is '-' or a digit. libc has no such flag, so it never reaches snprintf. + emitter.label("__rt_sprintf_padchar_linux_x86_64"); + emitter.instruction("cmp r13, 2"); // is there a byte after the quote? + emitter.instruction("jl __rt_sprintf_scan_width_linux_x86_64"); // malformed: leave it to the width scanner + emitter.instruction("movzx r8d, BYTE PTR [r12 + 1]"); // load the pad character + emitter.instruction("cmp r8b, 48"); // is the pad character '0'? + emitter.instruction("jne __rt_sprintf_padchar_custom_linux_x86_64"); // any other character pads uniformly + emitter.instruction("mov BYTE PTR [r10], 48"); // '0' is the zero flag: sign-aware, so libc keeps it + emitter.instruction("add r10, 1"); // advance the mini format cursor + emitter.instruction("jmp __rt_sprintf_padchar_done_linux_x86_64"); // and let snprintf do the padding + emitter.label("__rt_sprintf_padchar_custom_linux_x86_64"); + emitter.instruction("mov QWORD PTR [rbp - 264], r8"); // remember the pad character for the field renderer + emitter.label("__rt_sprintf_padchar_done_linux_x86_64"); + emitter.instruction("add r12, 2"); // consume the quote and the pad character + emitter.instruction("sub r13, 2"); // decrement the remaining format length + emitter.instruction("jmp __rt_sprintf_scan_flags_linux_x86_64"); // keep scanning flags + emitter.label("__rt_sprintf_copy_flag_linux_x86_64"); emitter.instruction("mov BYTE PTR [r10], r8b"); // append the current flag byte to the mini format string emitter.instruction("add r10, 1"); // advance the mini format-string cursor after appending one flag byte @@ -129,6 +203,7 @@ pub(super) fn emit_sprintf_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_sprintf_scan_dot_linux_x86_64"); emitter.instruction("cmp r8b, 46"); // is the next format byte the '.' introducer of a precision clause? emitter.instruction("jne __rt_sprintf_scan_type_linux_x86_64"); // skip precision parsing when the next format byte is not '.' + emitter.instruction("mov QWORD PTR [rbp - 232], r10"); // remember where the precision starts, so integer types can drop it emitter.instruction("mov BYTE PTR [r10], r8b"); // append the '.' precision introducer to the mini format string emitter.instruction("add r10, 1"); // advance the mini format-string cursor after appending the precision introducer emitter.instruction("add r12, 1"); // consume the precision introducer from the source format string @@ -167,55 +242,372 @@ pub(super) fn emit_sprintf_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("je __rt_sprintf_call_float_linux_x86_64"); // dispatch to the float snprintf path when the terminal type character is '%e' emitter.instruction("cmp r8b, 103"); // is the terminal type character '%g'? emitter.instruction("je __rt_sprintf_call_float_linux_x86_64"); // dispatch to the float snprintf path when the terminal type character is '%g' + emitter.instruction("cmp r8b, 69"); // is the terminal type character '%E'? + emitter.instruction("je __rt_sprintf_call_float_linux_x86_64"); // dispatch to the float snprintf path when the terminal type character is '%E' + emitter.instruction("cmp r8b, 71"); // is the terminal type character '%G'? + emitter.instruction("je __rt_sprintf_call_float_linux_x86_64"); // dispatch to the float snprintf path when the terminal type character is '%G' emitter.instruction("cmp r8b, 115"); // is the terminal type character '%s'? emitter.instruction("je __rt_sprintf_call_string_linux_x86_64"); // dispatch to the string snprintf path when the terminal type character is '%s' emitter.instruction("cmp r8b, 99"); // is the terminal type character '%c'? emitter.instruction("je __rt_sprintf_call_char_linux_x86_64"); // dispatch to the char snprintf path when the terminal type character is '%c' - emitter.instruction("jmp __rt_sprintf_call_int_linux_x86_64"); // treat all remaining supported type characters as integer-like snprintf operands + emitter.instruction("cmp r8b, 98"); // is the terminal type character '%b'? + emitter.instruction("je __rt_sprintf_type_b_linux_x86_64"); // php's binary conversion, which C has no equivalent for + emitter.instruction("jmp __rt_sprintf_int_prec_linux_x86_64"); // treat all remaining supported type characters as integer-like snprintf operands + + // -- flags php parses but ignores: consumed without reaching snprintf -- + emitter.label("__rt_sprintf_drop_flag_linux_x86_64"); + emitter.instruction("add r12, 1"); // consume the flag from the source format string + emitter.instruction("sub r13, 1"); // decrement the remaining format length + emitter.instruction("jmp __rt_sprintf_scan_flags_linux_x86_64"); // keep scanning flags + + // -- integer conversions: php does not give precision the C meaning -- + // %d, %u and %c ignore it outright; %x, %X and %o render nothing at all. + emitter.label("__rt_sprintf_int_prec_linux_x86_64"); + emitter.instruction("cmp QWORD PTR [rbp - 232], 0"); // was a precision scanned? + emitter.instruction("je __rt_sprintf_call_int_linux_x86_64"); // no precision: format the integer as-is + emitter.instruction("cmp r8b, 120"); // is the conversion 'x'? + emitter.instruction("je __rt_sprintf_int_blank_linux_x86_64"); // php renders nothing for a precise hex + emitter.instruction("cmp r8b, 88"); // is the conversion 'X'? + emitter.instruction("je __rt_sprintf_int_blank_linux_x86_64"); // php renders nothing for a precise hex + emitter.instruction("cmp r8b, 111"); // is the conversion 'o'? + emitter.instruction("je __rt_sprintf_int_blank_linux_x86_64"); // php renders nothing for a precise octal + emitter.instruction("mov r10, QWORD PTR [rbp - 232]"); // drop the precision so snprintf never sees it + emitter.instruction("mov BYTE PTR [r10], r8b"); // re-append the conversion character + emitter.instruction("add r10, 1"); // advance past it + emitter.instruction("mov BYTE PTR [r10], 0"); // re-terminate the shortened mini format string + emitter.instruction("jmp __rt_sprintf_call_int_linux_x86_64"); // format the integer without the precision + + // -- %b: php's binary conversion, which C's formatter does not have -- + // See the AArch64 arm: unsigned 64-bit expansion, then the same field renderer + // %s uses so width, alignment and the pad character behave identically. + emitter.label("__rt_sprintf_type_b_linux_x86_64"); + emitter.instruction("cmp QWORD PTR [rbp - 232], 0"); // was a precision scanned? + emitter.instruction("jne __rt_sprintf_int_blank_linux_x86_64"); // a precision renders nothing, as for %x and %o + emitter.instruction("mov r11, QWORD PTR [r9]"); // load the integer payload + emitter.instruction("lea rsi, [rbp - 160]"); // build backwards from the far end of the scratch buffer + emitter.instruction("xor ecx, ecx"); // digit count + emitter.label("__rt_sprintf_b_digit_linux_x86_64"); + emitter.instruction("mov r8, r11"); // copy the remaining value + emitter.instruction("and r8, 1"); // take the low bit + emitter.instruction("add r8, 48"); // render it as '0' or '1' + emitter.instruction("sub rsi, 1"); // move one byte left + emitter.instruction("mov BYTE PTR [rsi], r8b"); // store the digit + emitter.instruction("add rcx, 1"); // count it + emitter.instruction("shr r11, 1"); // shift the value right, unsigned + emitter.instruction("jnz __rt_sprintf_b_digit_linux_x86_64"); // keep going while bits remain + emitter.instruction("jmp __rt_sprintf_field_render_linux_x86_64"); // pad and align exactly as %s does + + emitter.label("__rt_sprintf_int_blank_linux_x86_64"); + emitter.instruction("mov r10, QWORD PTR [rbp - 232]"); // drop the precision from the scanned prefix + emitter.instruction("xor ecx, ecx"); // nothing to render: the field is pure padding + emitter.instruction("jmp __rt_sprintf_field_render_linux_x86_64"); // reuse the string field renderer emitter.label("__rt_sprintf_end_spec_linux_x86_64"); emitter.instruction("jmp __rt_sprintf_done_linux_x86_64"); // stop formatting when the format string ends partway through a specifier emitter.label("__rt_sprintf_call_float_linux_x86_64"); - emitter.instruction("movq xmm0, QWORD PTR [r9]"); // load the packed floating-point bits into xmm0 for the SysV variadic snprintf call + emitter.instruction("mov r11, QWORD PTR [r9]"); // stage the variadic payload so an oversized render can repeat the call + emitter.instruction("mov QWORD PTR [rbp - 240], r11"); // SysV passes it in registers, which the first snprintf call clobbers + emitter.instruction("movq xmm0, QWORD PTR [r9]"); // load the packed floating-point bits into xmm0 for the variadic snprintf call emitter.instruction("lea rdi, [rbp - 224]"); // point snprintf at the fixed local output scratch buffer emitter.instruction("mov esi, 128"); // bound the local snprintf output scratch buffer to 128 bytes emitter.instruction("lea rdx, [rbp - 96]"); // pass the one-specifier mini format string to snprintf as the format pointer - emitter.instruction("mov eax, 1"); // advertise one live SIMD variadic register to the SysV variadic call ABI - emitter.bl_c("snprintf"); // format the floating operand into the local snprintf output scratch buffer - emitter.instruction("jmp __rt_sprintf_copy_result_linux_x86_64"); // copy the freshly formatted snprintf output into the concat buffer + if emitter.platform == Platform::Windows { + // This call has NO leading integer precision argument (the user's mini + // format string already embeds precision/width/flags), so the double is + // MSx64 positional argument 4 — `__rt_sys_snprintf` (register-shaped for + // __rt_ftoa's `snprintf(buf, size, "%.*e", precision, double)`, where the + // double is positional argument 5) would misroute it; call the + // dedicated `__rt_sys_snprintf_double` shim instead (WF10b BUG A fix). + emitter.instruction("call __rt_sys_snprintf_double"); // route through the 3-fixed-arg + 1-variadic-double MSx64 shim + } else { + emitter.instruction("mov eax, 1"); // advertise one live SIMD variadic register to the SysV variadic call ABI + emitter.emit_call_c("snprintf"); // format the floating operand into the local snprintf output scratch buffer + } + // -- PHP parity: %e/%E (and any exponential-form %g/%G) exponent uses the + // -- minimum digit count (no leading zero), but CRT snprintf pads to at + // -- least 2 digits. A double's decimal exponent never exceeds 3 digits and + // -- 3-digit exponents are never zero-padded (they start at magnitude 100), + // -- so the only possible padding is a single leading '0' in a 2-digit + // -- exponent; strip it in place and shrink the byte count by one. + emitter.instruction("lea rsi, [rbp - 224]"); // scan cursor over the freshly formatted snprintf output + emitter.instruction("mov rcx, rax"); // remaining bytes to scan for the 'e'/'E' exponent marker + emitter.label("__rt_sprintf_etrim_scan_linux_x86_64"); + emitter.instruction("test rcx, rcx"); // scanned the whole output without finding an exponent marker? + emitter.instruction("jz __rt_sprintf_etrim_done_linux_x86_64"); // no exponent (e.g. %f) -> nothing to trim + emitter.instruction("movzx edx, BYTE PTR [rsi]"); // load the next output byte + emitter.instruction("cmp dl, 101"); // is it 'e'? + emitter.instruction("je __rt_sprintf_etrim_found_linux_x86_64"); // found the exponent marker + emitter.instruction("cmp dl, 69"); // is it 'E'? + emitter.instruction("je __rt_sprintf_etrim_found_linux_x86_64"); // found the exponent marker + emitter.instruction("inc rsi"); // advance the scan cursor + emitter.instruction("dec rcx"); // decrement the remaining scan length + emitter.instruction("jmp __rt_sprintf_etrim_scan_linux_x86_64"); // keep scanning for the exponent marker + emitter.label("__rt_sprintf_etrim_found_linux_x86_64"); + emitter.instruction("inc rsi"); // advance past the 'e'/'E' marker + emitter.instruction("dec rcx"); // decrement the remaining scan length + emitter.instruction("test rcx, rcx"); // malformed: exponent marker was the last byte? + emitter.instruction("jz __rt_sprintf_etrim_done_linux_x86_64"); // bail out defensively + emitter.instruction("movzx edx, BYTE PTR [rsi]"); // load the byte after the exponent marker + emitter.instruction("cmp dl, 43"); // is it '+'? + emitter.instruction("je __rt_sprintf_etrim_sign_linux_x86_64"); // consume the exponent sign + emitter.instruction("cmp dl, 45"); // is it '-'? + emitter.instruction("jne __rt_sprintf_etrim_done_linux_x86_64"); // C99 always emits an exponent sign; bail defensively if absent + emitter.label("__rt_sprintf_etrim_sign_linux_x86_64"); + emitter.instruction("inc rsi"); // advance past the exponent sign + emitter.instruction("dec rcx"); // decrement the remaining scan length + emitter.instruction("cmp rcx, 2"); // need at least two remaining bytes to test "0" + emitter.instruction("jl __rt_sprintf_etrim_done_linux_x86_64"); // too short to be a padded 2-digit exponent + emitter.instruction("movzx edx, BYTE PTR [rsi]"); // load the first exponent digit + emitter.instruction("cmp dl, 48"); // is it '0'? + emitter.instruction("jne __rt_sprintf_etrim_done_linux_x86_64"); // not zero-padded -> nothing to strip + emitter.instruction("movzx edx, BYTE PTR [rsi + 1]"); // load the next byte after the leading zero + emitter.instruction("cmp dl, 48"); // is it below '0'? + emitter.instruction("jl __rt_sprintf_etrim_done_linux_x86_64"); // not a digit -> the '0' was the only exponent digit, keep it + emitter.instruction("cmp dl, 57"); // is it above '9'? + emitter.instruction("jg __rt_sprintf_etrim_done_linux_x86_64"); // not a digit -> keep the only exponent digit + // -- guard: a right-justified WIDTH field pads BEFORE the sign/mantissa with + // -- ' ' or '0'. Stripping a byte from the exponent would shrink the total + // -- field width, so detect that padding and skip the strip entirely rather + // -- than corrupt the requested width (a documented, bounded residual gap; + // -- the no-width and left-justified-width cases below are fully handled). + emitter.instruction("lea r8, [rbp - 224]"); // buffer start + emitter.instruction("movzx edx, BYTE PTR [r8]"); // first output byte + emitter.instruction("cmp dl, 32"); // is it a space (space-padded field)? + emitter.instruction("je __rt_sprintf_etrim_done_linux_x86_64"); // space padding present -> skip the strip + emitter.instruction("mov r10, r8"); // cursor for the (optional) leading '-'/'+' sign + emitter.instruction("cmp dl, 45"); // is the first byte a '-' sign? + emitter.instruction("je __rt_sprintf_etrim_lead_sign_linux_x86_64"); // yes -> skip past it before checking for zero-padding + emitter.instruction("cmp dl, 43"); // is the first byte a '+' sign (the '+' flag)? + emitter.instruction("jne __rt_sprintf_etrim_lead_check_linux_x86_64"); // no sign -> check directly + emitter.label("__rt_sprintf_etrim_lead_sign_linux_x86_64"); + emitter.instruction("inc r10"); // skip past the sign before checking for zero-padding + emitter.label("__rt_sprintf_etrim_lead_check_linux_x86_64"); + emitter.instruction("movzx edx, BYTE PTR [r10]"); // byte after the optional sign + emitter.instruction("cmp dl, 48"); // is it '0'? + emitter.instruction("jne __rt_sprintf_etrim_shift_setup_linux_x86_64"); // not zero -> no zero-padding, safe to strip + emitter.instruction("movzx edx, BYTE PTR [r10 + 1]"); // byte after that leading zero + emitter.instruction("cmp dl, 46"); // is it '.' (the leading zero IS the legitimate mantissa digit)? + emitter.instruction("je __rt_sprintf_etrim_shift_setup_linux_x86_64"); // legitimate "0.xxx" mantissa -> safe to strip the exponent + emitter.instruction("jmp __rt_sprintf_etrim_done_linux_x86_64"); // zero-padded width field -> skip the strip + // -- confirmed a padded 2-digit exponent ("0" + digit) with no right-justify padding: shift the tail left by one byte, dropping the leading zero -- + emitter.label("__rt_sprintf_etrim_shift_setup_linux_x86_64"); + emitter.instruction("lea r8, [rbp - 224]"); // recompute the scratch buffer base + emitter.instruction("add r8, rax"); // r8 = original buffer end (using the pre-trim snprintf byte count) + emitter.instruction("movzx edx, BYTE PTR [r8 - 1]"); // last output byte (before this trim) + emitter.instruction("cmp dl, 32"); // was the field left-justify space-padded? + emitter.instruction("sete dil"); // dil = 1 when trailing-space padding is present + emitter.instruction("movzx edi, dil"); // zero-extend the trailing-padding flag + emitter.instruction("lea r9, [rsi + 1]"); // source cursor = byte after the leading zero + emitter.instruction("mov r10, rsi"); // dest cursor = the leading zero's position + emitter.label("__rt_sprintf_etrim_shift_linux_x86_64"); + emitter.instruction("cmp r9, r8"); // reached the end of the original output? + emitter.instruction("jge __rt_sprintf_etrim_shift_done_linux_x86_64"); // shift complete + emitter.instruction("movzx r11d, BYTE PTR [r9]"); // load the next byte to shift down + emitter.instruction("mov BYTE PTR [r10], r11b"); // shift it left by one position + emitter.instruction("inc r9"); // advance the source cursor + emitter.instruction("inc r10"); // advance the dest cursor + emitter.instruction("jmp __rt_sprintf_etrim_shift_linux_x86_64"); // continue shifting + emitter.label("__rt_sprintf_etrim_shift_done_linux_x86_64"); + emitter.instruction("test rdi, rdi"); // was trailing padding preserved? + emitter.instruction("jz __rt_sprintf_etrim_shrink_linux_x86_64"); // no padding -> just shrink the byte count + emitter.instruction("mov BYTE PTR [r10], 32"); // restore the requested field width with one more trailing pad space + emitter.instruction("jmp __rt_sprintf_etrim_done_linux_x86_64"); // keep rax unchanged: total field width is preserved + emitter.label("__rt_sprintf_etrim_shrink_linux_x86_64"); + emitter.instruction("dec rax"); // no padding to preserve -> one byte shorter after dropping the leading exponent zero + emitter.label("__rt_sprintf_etrim_done_linux_x86_64"); + emitter.instruction("jmp __rt_sprintf_copy_result_linux_x86_64"); // copy the freshly formatted (and exponent-trimmed) snprintf output into the concat buffer + // php integers are 64-bit, so the conversion needs C's "ll" length modifier. + // Without it snprintf reads 32 bits and sprintf("%u", -1) renders 4294967295 + // instead of php's 18446744073709551615. The AArch64 arm has always written it; + // this arm never did, and no fixture used a value whose high half mattered. + // + // Both paths reaching this label leave the conversion character at [r10 - 1], + // the terminator at [r10], and the character itself still in r8b, so the two + // bytes are spliced in place instead of being threaded through the scanner. + // %c does not come through here -- it has its own call site, matching C, where + // the argument is an int rather than a php integer. emitter.label("__rt_sprintf_call_int_linux_x86_64"); + emitter.instruction("mov BYTE PTR [r10 - 1], 108"); // overwrite the conversion with the first 'l' + emitter.instruction("mov BYTE PTR [r10], 108"); // append the second 'l' + emitter.instruction("mov BYTE PTR [r10 + 1], r8b"); // restore the conversion character after the modifier + emitter.instruction("mov BYTE PTR [r10 + 2], 0"); // re-terminate the widened mini format string emitter.instruction("lea rdi, [rbp - 224]"); // point snprintf at the fixed local output scratch buffer emitter.instruction("mov esi, 128"); // bound the local snprintf output scratch buffer to 128 bytes emitter.instruction("lea rdx, [rbp - 96]"); // pass the one-specifier mini format string to snprintf as the format pointer + emitter.instruction("mov r11, QWORD PTR [r9]"); // stage the variadic payload so an oversized render can repeat the call + emitter.instruction("mov QWORD PTR [rbp - 240], r11"); // SysV passes it in registers, which the first snprintf call clobbers emitter.instruction("mov rcx, QWORD PTR [r9]"); // load the packed integer payload into the first SysV variadic integer register emitter.instruction("xor eax, eax"); // advertise that no SIMD variadic registers are live for the integer snprintf call - emitter.bl_c("snprintf"); // format the integer-like operand into the local snprintf output scratch buffer + emitter.emit_call_c("snprintf"); // format the integer-like operand into the local snprintf output scratch buffer emitter.instruction("jmp __rt_sprintf_copy_result_linux_x86_64"); // copy the freshly formatted snprintf output into the concat buffer emitter.label("__rt_sprintf_call_char_linux_x86_64"); emitter.instruction("lea rdi, [rbp - 224]"); // point snprintf at the fixed local output scratch buffer emitter.instruction("mov esi, 128"); // bound the local snprintf output scratch buffer to 128 bytes emitter.instruction("lea rdx, [rbp - 96]"); // pass the one-specifier mini format string to snprintf as the format pointer + emitter.instruction("mov r11, QWORD PTR [r9]"); // stage the variadic payload so an oversized render can repeat the call + emitter.instruction("mov QWORD PTR [rbp - 240], r11"); // SysV passes it in registers, which the first snprintf call clobbers emitter.instruction("mov ecx, DWORD PTR [r9]"); // load the packed character payload into the first SysV variadic integer register emitter.instruction("xor eax, eax"); // advertise that no SIMD variadic registers are live for the char snprintf call - emitter.bl_c("snprintf"); // format the character operand into the local snprintf output scratch buffer + emitter.emit_call_c("snprintf"); // format the character operand into the local snprintf output scratch buffer emitter.instruction("jmp __rt_sprintf_copy_result_linux_x86_64"); // copy the freshly formatted snprintf output into the concat buffer + // php's %s is only "truncate to precision, then pad to width with the pad + // character", so it needs no C formatter. Routing it through __rt_cstr and + // snprintf truncated every argument at the scratch buffer's capacity and cut the + // string short at any NUL byte -- php strings are binary and may contain them. + // The bytes now go straight to the destination using the stored length. + // + // The scan loops need no end pointer: the terminal type character stops each of + // them, being neither a flag byte nor a digit. emitter.label("__rt_sprintf_call_string_linux_x86_64"); - emitter.instruction("mov rax, QWORD PTR [r9]"); // load the packed elephc string pointer before converting it into a null-terminated C string - emitter.instruction("mov rdx, QWORD PTR [r9 + 8]"); // load the packed string metadata word before extracting the elephc string length - emitter.instruction("shr rdx, 8"); // extract the original elephc string length from the packed string metadata word - emitter.instruction("call __rt_cstr"); // convert the elephc string pointer+length pair into a null-terminated C string in the scratch buffer - emitter.instruction("lea rdi, [rbp - 224]"); // point snprintf at the fixed local output scratch buffer - emitter.instruction("mov esi, 128"); // bound the local snprintf output scratch buffer to 128 bytes - emitter.instruction("lea rdx, [rbp - 96]"); // pass the one-specifier mini format string to snprintf as the format pointer - emitter.instruction("mov rcx, rax"); // pass the null-terminated C string pointer in the first SysV variadic integer register - emitter.instruction("xor eax, eax"); // advertise that no SIMD variadic registers are live for the string snprintf call - emitter.bl_c("snprintf"); // format the string operand into the local snprintf output scratch buffer + emitter.instruction("mov rsi, QWORD PTR [r9]"); // load the packed elephc string pointer + emitter.instruction("mov rcx, QWORD PTR [r9 + 8]"); // load the packed string metadata word + emitter.instruction("shr rcx, 8"); // extract the elephc string length + emitter.label("__rt_sprintf_field_render_linux_x86_64"); + emitter.instruction("lea rdi, [rbp - 95]"); // first mini-format byte after the '%' + emitter.instruction("xor r8d, r8d"); // left-align flag, clear by default + emitter.instruction("mov edx, 32"); // pad character, space by default + emitter.instruction("mov r9, QWORD PTR [rbp - 264]"); // did the specifier name its own pad character? + emitter.instruction("cmp r9, -1"); // -1 means it did not + emitter.instruction("cmovne rdx, r9"); // php's \"'X\" overrides the default + emitter.instruction("xor eax, eax"); // field width + emitter.instruction("mov r11, -1"); // precision, -1 when absent + emitter.label("__rt_sprintf_str_flag_linux_x86_64"); + emitter.instruction("movzx r9d, BYTE PTR [rdi]"); // load the next mini-format byte + emitter.instruction("cmp r9b, 45"); // is it '-'? + emitter.instruction("jne __rt_sprintf_str_flag_zero_linux_x86_64"); // not the left-align flag + emitter.instruction("mov r8d, 1"); // left-align the field + emitter.instruction("add rdi, 1"); // consume the flag + emitter.instruction("jmp __rt_sprintf_str_flag_linux_x86_64"); // keep reading flags + emitter.label("__rt_sprintf_str_flag_zero_linux_x86_64"); + emitter.instruction("cmp r9b, 48"); // is it '0'? + emitter.instruction("jne __rt_sprintf_str_flag_skip_linux_x86_64"); // not the zero-pad flag + emitter.instruction("mov edx, 48"); // pad with '0' instead of space + emitter.instruction("add rdi, 1"); // consume the flag + emitter.instruction("jmp __rt_sprintf_str_flag_linux_x86_64"); // keep reading flags + emitter.label("__rt_sprintf_str_flag_skip_linux_x86_64"); + emitter.instruction("cmp r9b, 43"); // is it '+'? + emitter.instruction("je __rt_sprintf_str_flag_drop_linux_x86_64"); // php ignores '+' on strings + emitter.instruction("cmp r9b, 32"); // is it ' '? + emitter.instruction("jne __rt_sprintf_str_width_linux_x86_64"); // no flags left: read the width + emitter.label("__rt_sprintf_str_flag_drop_linux_x86_64"); + emitter.instruction("add rdi, 1"); // consume the ignored flag + emitter.instruction("jmp __rt_sprintf_str_flag_linux_x86_64"); // keep reading flags + emitter.label("__rt_sprintf_str_width_linux_x86_64"); + emitter.instruction("movzx r9d, BYTE PTR [rdi]"); // load the next mini-format byte + emitter.instruction("cmp r9b, 46"); // is it the precision '.'? + emitter.instruction("je __rt_sprintf_str_prec_start_linux_x86_64"); // switch to reading the precision + emitter.instruction("cmp r9b, 48"); // below '0'? + emitter.instruction("jb __rt_sprintf_str_field_linux_x86_64"); // not a digit: the width is complete + emitter.instruction("cmp r9b, 57"); // above '9'? + emitter.instruction("ja __rt_sprintf_str_field_linux_x86_64"); // not a digit: the width is complete + emitter.instruction("imul rax, rax, 10"); // shift the accumulated width one decimal place + emitter.instruction("sub r9d, 48"); // digit value + emitter.instruction("add rax, r9"); // accumulate the width digit + emitter.instruction("add rdi, 1"); // consume the digit + emitter.instruction("jmp __rt_sprintf_str_width_linux_x86_64"); // keep reading width digits + emitter.label("__rt_sprintf_str_prec_start_linux_x86_64"); + emitter.instruction("add rdi, 1"); // consume the '.' + emitter.instruction("xor r11d, r11d"); // an explicit precision starts at zero + emitter.label("__rt_sprintf_str_prec_linux_x86_64"); + emitter.instruction("movzx r9d, BYTE PTR [rdi]"); // load the next mini-format byte + emitter.instruction("cmp r9b, 48"); // below '0'? + emitter.instruction("jb __rt_sprintf_str_field_linux_x86_64"); // not a digit: the precision is complete + emitter.instruction("cmp r9b, 57"); // above '9'? + emitter.instruction("ja __rt_sprintf_str_field_linux_x86_64"); // not a digit: the precision is complete + emitter.instruction("imul r11, r11, 10"); // shift the accumulated precision one decimal place + emitter.instruction("sub r9d, 48"); // digit value + emitter.instruction("add r11, r9"); // accumulate the precision digit + emitter.instruction("add rdi, 1"); // consume the digit + emitter.instruction("jmp __rt_sprintf_str_prec_linux_x86_64"); // keep reading precision digits + emitter.label("__rt_sprintf_str_field_linux_x86_64"); + emitter.instruction("cmp r11, -1"); // was a precision given? + emitter.instruction("je __rt_sprintf_str_pad_count_linux_x86_64"); // no precision: keep the whole string + emitter.instruction("cmp rcx, r11"); // is the string longer than the precision? + emitter.instruction("cmovg rcx, r11"); // php truncates the string to the precision + emitter.label("__rt_sprintf_str_pad_count_linux_x86_64"); + emitter.instruction("sub rax, rcx"); // padding = width - rendered length + emitter.instruction("mov r9, 0"); // materialize zero for the padding clamp + emitter.instruction("cmp rax, 0"); // is the field already at least as wide? + emitter.instruction("cmovl rax, r9"); // never pad a negative amount + emitter.instruction("test r8d, r8d"); // is the field left-aligned? + emitter.instruction("jnz __rt_sprintf_str_copy_first_linux_x86_64"); // left-align writes the bytes first + emitter.label("__rt_sprintf_str_pad_lead_linux_x86_64"); + emitter.instruction("test rax, rax"); // any leading padding left? + emitter.instruction("jz __rt_sprintf_str_copy_tail_linux_x86_64"); // leading padding written + emitter.instruction("mov BYTE PTR [rbx], dl"); // write one pad character + emitter.instruction("add rbx, 1"); // advance the destination cursor + emitter.instruction("sub rax, 1"); // one fewer pad character to write + emitter.instruction("jmp __rt_sprintf_str_pad_lead_linux_x86_64"); // keep padding + emitter.label("__rt_sprintf_str_copy_tail_linux_x86_64"); + emitter.instruction("test rcx, rcx"); // any argument bytes left? + emitter.instruction("jz __rt_sprintf_loop_linux_x86_64"); // the whole string is written + emitter.instruction("mov r9b, BYTE PTR [rsi]"); // load one argument byte + emitter.instruction("mov BYTE PTR [rbx], r9b"); // append it to the destination + emitter.instruction("add rsi, 1"); // advance the argument cursor + emitter.instruction("add rbx, 1"); // advance the destination cursor + emitter.instruction("sub rcx, 1"); // one fewer byte to copy + emitter.instruction("jmp __rt_sprintf_str_copy_tail_linux_x86_64"); // keep copying + emitter.label("__rt_sprintf_str_copy_first_linux_x86_64"); + emitter.instruction("test rcx, rcx"); // any argument bytes left? + emitter.instruction("jz __rt_sprintf_str_pad_trail_linux_x86_64"); // the whole string is written + emitter.instruction("mov r9b, BYTE PTR [rsi]"); // load one argument byte + emitter.instruction("mov BYTE PTR [rbx], r9b"); // append it to the destination + emitter.instruction("add rsi, 1"); // advance the argument cursor + emitter.instruction("add rbx, 1"); // advance the destination cursor + emitter.instruction("sub rcx, 1"); // one fewer byte to copy + emitter.instruction("jmp __rt_sprintf_str_copy_first_linux_x86_64"); // keep copying + emitter.label("__rt_sprintf_str_pad_trail_linux_x86_64"); + emitter.instruction("test rax, rax"); // any trailing padding left? + emitter.instruction("jz __rt_sprintf_loop_linux_x86_64"); // trailing padding written + emitter.instruction("mov BYTE PTR [rbx], dl"); // write one pad character + emitter.instruction("add rbx, 1"); // advance the destination cursor + emitter.instruction("sub rax, 1"); // one fewer pad character to write + emitter.instruction("jmp __rt_sprintf_str_pad_trail_linux_x86_64"); // keep padding emitter.label("__rt_sprintf_copy_result_linux_x86_64"); + + // snprintf reports the length it *would* have written, which can exceed the + // 128-byte scratch. Copying that many bytes reads past the buffer and emits + // adjacent stack memory, so an oversized result is re-rendered at the + // destination, which has the whole 64 KiB concat buffer behind it. + emitter.instruction("cmp rax, 128"); // did the whole result fit in the scratch buffer? + emitter.instruction("jge __rt_sprintf_overflow_linux_x86_64"); // it did not: render it at the destination + // -- php's "'X" pad character on a conversion libc rendered -- + // libc has no such flag, so snprintf padded with spaces. A numeric conversion + // never contains a space of its own once the space flag is dropped, so the + // padding is exactly the run of spaces at one end or the other. + emitter.instruction("mov r9, QWORD PTR [rbp - 264]"); // did the specifier name a pad character? + emitter.instruction("cmp r9, -1"); // -1 means it did not + emitter.instruction("je __rt_sprintf_pad_done_linux_x86_64"); // leave the spaces alone + emitter.instruction("lea rsi, [rbp - 224]"); // scan the freshly rendered output + emitter.instruction("mov rdi, rax"); // bytes rendered + emitter.label("__rt_sprintf_pad_lead_linux_x86_64"); + emitter.instruction("test rdi, rdi"); // anything left to inspect? + emitter.instruction("jz __rt_sprintf_pad_done_linux_x86_64"); // the whole field was padding + emitter.instruction("cmp BYTE PTR [rsi], 32"); // is it a padding space? + emitter.instruction("jne __rt_sprintf_pad_trail_linux_x86_64"); // the value starts here: try the other end + emitter.instruction("mov BYTE PTR [rsi], r9b"); // substitute the pad character + emitter.instruction("add rsi, 1"); // advance + emitter.instruction("sub rdi, 1"); // one fewer byte to inspect + emitter.instruction("jmp __rt_sprintf_pad_lead_linux_x86_64"); // keep substituting + emitter.label("__rt_sprintf_pad_trail_linux_x86_64"); + emitter.instruction("lea rsi, [rbp - 224]"); // back to the start of the output + emitter.instruction("add rsi, rax"); // one past its end + emitter.instruction("mov rdi, rax"); // bytes still to inspect + emitter.label("__rt_sprintf_pad_trail_loop_linux_x86_64"); + emitter.instruction("test rdi, rdi"); // anything left? + emitter.instruction("jz __rt_sprintf_pad_done_linux_x86_64"); // nothing left + emitter.instruction("sub rsi, 1"); // step back one byte + emitter.instruction("cmp BYTE PTR [rsi], 32"); // is it a padding space? + emitter.instruction("jne __rt_sprintf_pad_done_linux_x86_64"); // the value ends here + emitter.instruction("mov BYTE PTR [rsi], r9b"); // substitute the pad character + emitter.instruction("sub rdi, 1"); // one fewer byte to inspect + emitter.instruction("jmp __rt_sprintf_pad_trail_loop_linux_x86_64"); // keep substituting + emitter.label("__rt_sprintf_pad_done_linux_x86_64"); emitter.instruction("mov rcx, rax"); // copy the snprintf-written byte count before the result-copy loop consumes caller-saved registers emitter.instruction("lea r10, [rbp - 224]"); // point at the local snprintf output scratch buffer before copying its bytes into the concat buffer emitter.label("__rt_sprintf_copy_loop_linux_x86_64"); @@ -228,6 +620,32 @@ pub(super) fn emit_sprintf_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("sub rcx, 1"); // decrement the remaining snprintf output byte count after copying one byte emitter.instruction("jmp __rt_sprintf_copy_loop_linux_x86_64"); // continue copying snprintf output until every byte has been appended to the concat buffer + // ================================================================ + // OVERFLOW: the conversion did not fit the scratch buffer + // The variadic argument registers are re-established, so the same mini format is + // rendered a second time, writing at the destination cursor with the concat + // buffer's remaining capacity as the bound. + // ================================================================ + emitter.label("__rt_sprintf_overflow_linux_x86_64"); + emitter.instruction("mov rdi, rbx"); // write at the destination cursor + emitter.instruction("mov r10, QWORD PTR [rbp - 56]"); // reload the concat-offset symbol address + emitter.instruction("mov rsi, 65536"); // total concat buffer size + emitter.instruction("mov r11, QWORD PTR [rbp - 48]"); // start of this result inside the concat buffer + emitter.instruction("sub r11, rbx"); // negated bytes emitted so far for this result + emitter.instruction("add rsi, r11"); // remaining capacity, including the terminator + emitter.instruction("lea rdx, [rbp - 96]"); // the same one-specifier mini format string + emitter.instruction("mov rcx, QWORD PTR [rbp - 240]"); // restore the variadic payload as the integer argument + emitter.instruction("movq xmm0, QWORD PTR [rbp - 240]"); // and as the floating-point argument; snprintf reads the one the format names + emitter.instruction("mov eax, 1"); // one SIMD variadic register is live for the re-render + emitter.emit_call_c("snprintf"); // re-render at the destination + emitter.instruction("cmp rax, rsi"); // did the second render fill the remaining capacity? + emitter.instruction("jl __rt_sprintf_overflow_done_linux_x86_64"); // it fit: advance by what was written + emitter.instruction("mov rax, rsi"); // clamp to the remaining capacity + emitter.instruction("sub rax, 1"); // excluding the terminator snprintf wrote + emitter.label("__rt_sprintf_overflow_done_linux_x86_64"); + emitter.instruction("add rbx, rax"); // advance the destination cursor past the rendered value + emitter.instruction("jmp __rt_sprintf_loop_linux_x86_64"); // continue scanning the format string + emitter.label("__rt_sprintf_done_linux_x86_64"); emitter.instruction("mov rax, QWORD PTR [rbp - 48]"); // return the concat-buffer start pointer of the formatted string in the primary x86_64 string result register emitter.instruction("mov rdx, rbx"); // copy the concat-buffer end cursor so the final formatted-string length can be derived @@ -250,3 +668,170 @@ pub(super) fn emit_sprintf_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("push r11"); // recreate the preserved return address at the top of the rethreaded stack so a plain ret lands back in generated code emitter.instruction("ret"); // return the formatted string in the standard x86_64 string result registers while also discarding the caller-owned tagged arguments } + +#[cfg(test)] +mod tests { + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::*; + + /// Regression test for the sprintf snprintf-call Class-1 ABI bug (WF2/F2): on + /// windows-x86_64, `bl_c("snprintf")` reached the raw msvcrt import with + /// SysV-staged arguments (rdi/rsi/rdx/rcx/xmm0) instead of the MSx64 ABI that + /// msvcrt `snprintf` expects (rcx/rdx/r8/r9), corrupting every sprintf-formatted + /// value. `emit_call_c("snprintf")` routes the call through the + /// `__rt_sys_snprintf` shim instead, which performs the SysV->MSx64 conversion. + #[test] + fn test_emit_sprintf_windows_x86_64_routes_snprintf_through_shim() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_sprintf_linux_x86_64(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains("call __rt_sys_snprintf\n")); + assert!(!asm.contains("call snprintf\n")); + } + + /// Companion non-Windows control: on Linux x86_64 (and every other non-Windows + /// target), `emit_call_c` is byte-identical to `bl_c`, so the format-specifier + /// dispatch paths must still emit a bare `call snprintf` unchanged. + #[test] + fn test_emit_sprintf_linux_x86_64_still_calls_bare_snprintf() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_sprintf_linux_x86_64(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains("call snprintf\n")); + assert!(!asm.contains("__rt_sys_snprintf")); + } + + /// Regression test for WF10b BUG A ("windows garbage output"): the `__rt_sprintf` + /// float path (`%f`/`%e`/`%g`/`%E`/`%G`) has NO leading integer precision + /// argument — its `snprintf(buf, size, fmt, double)` call shape differs from + /// `__rt_ftoa`'s `snprintf(buf, size, "%.*e", precision, double)` shape, so it + /// must route through the dedicated `__rt_sys_snprintf_double` shim on + /// windows-x86_64, NOT the generic `__rt_sys_snprintf` shim (which would stage + /// the double at the wrong argument position and produce garbage). + #[test] + fn test_emit_sprintf_windows_x86_64_float_path_routes_through_snprintf_double_shim() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_sprintf_linux_x86_64(&mut emitter); + let asm = emitter.output(); + + let float_start = asm + .find("__rt_sprintf_call_float_linux_x86_64:\n") + .expect("float dispatch label missing"); + let float_end = asm[float_start..] + .find("__rt_sprintf_call_int_linux_x86_64:\n") + .map(|offset| float_start + offset) + .expect("int dispatch label missing after float section"); + let float_section = &asm[float_start..float_end]; + + assert!( + float_section.contains("call __rt_sys_snprintf_double\n"), + "the float path must call the dedicated double-arg4 shim" + ); + assert!( + !float_section.contains("call __rt_sys_snprintf\n"), + "the float path must NOT reach the generic (precision-int-shaped) snprintf shim" + ); + } + + /// Companion non-Windows control: the float path's SysV variadic staging + /// (`mov eax, 1` then a bare `call snprintf`) must stay unchanged on Linux/macOS. + #[test] + fn test_emit_sprintf_linux_x86_64_float_path_still_calls_bare_snprintf() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_sprintf_linux_x86_64(&mut emitter); + let asm = emitter.output(); + + let float_start = asm + .find("__rt_sprintf_call_float_linux_x86_64:\n") + .expect("float dispatch label missing"); + let float_end = asm[float_start..] + .find("__rt_sprintf_call_int_linux_x86_64:\n") + .map(|offset| float_start + offset) + .expect("int dispatch label missing after float section"); + let float_section = &asm[float_start..float_end]; + + assert!(float_section.contains("mov eax, 1\n")); + assert!(float_section.contains("call snprintf\n")); + assert!(!float_section.contains("__rt_sys_snprintf_double")); + } + + /// Verifies the `%E`/`%G` uppercase specifiers dispatch to the float path + /// alongside `%f`/`%e`/`%g` (a WF10b fix: they previously fell through to the + /// integer path, reinterpreting the double's raw bits as an integer and + /// producing garbage — e.g. `sprintf("%E", 1.0)` read the mantissa bit pattern + /// as an int64 instead of formatting the float). + #[test] + fn test_emit_sprintf_dispatches_uppercase_e_and_g_to_float_path() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_sprintf_linux_x86_64(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains("cmp r8b, 69\n"), "'E' (69) must be checked"); + assert!(asm.contains("cmp r8b, 71\n"), "'G' (71) must be checked"); + } + + /// Verifies the `%e`/`%E` exponent-trim: PHP's minimum-digit exponent + /// (`1.0e+4`, not CRT's `1.0e+04`) is produced by stripping a lone padded + /// leading zero from a 2-digit CRT exponent, guarded against corrupting an + /// explicit field WIDTH (see the padding-detection instructions before the + /// shift loop). + #[test] + fn test_emit_sprintf_float_path_has_exponent_trim_with_padding_guard() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_sprintf_linux_x86_64(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains("__rt_sprintf_etrim_scan_linux_x86_64:\n")); + assert!(asm.contains("__rt_sprintf_etrim_shift_setup_linux_x86_64:\n")); + assert!( + asm.contains("cmp dl, 32\n"), + "must detect space-padded (right-justified) fields to guard the strip" + ); + } + + /// Verifies the integer call site widens the conversion to `ll`. + /// + /// php integers are 64-bit. Without the length modifier snprintf reads 32 bits + /// and `sprintf("%u", -1)` renders `4294967295` instead of php's + /// `18446744073709551615`. This ran green on every AArch64 host for as long as + /// the arm existed, so the check lives here, in a unit test over the emitted + /// text, rather than only in a fixture that needs an x86_64 runner to fail. + /// + /// `%c` must keep the C width: it has its own call site, and the assertion that + /// the modifier is absent there is what keeps this from being applied blindly to + /// every conversion. + #[test] + fn test_emit_sprintf_integer_path_widens_conversion_to_64_bit() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_sprintf_linux_x86_64(&mut emitter); + let asm = emitter.output(); + + let int_start = asm + .find("__rt_sprintf_call_int_linux_x86_64:\n") + .expect("int dispatch label missing"); + let int_end = asm[int_start..] + .find("__rt_sprintf_call_char_linux_x86_64:\n") + .map(|offset| int_start + offset) + .expect("char dispatch label missing after int section"); + let int_section = &asm[int_start..int_end]; + + assert!( + int_section.contains("mov BYTE PTR [r10 - 1], 108\n") + && int_section.contains("mov BYTE PTR [r10], 108\n"), + "the integer conversion must be widened with the 'll' length modifier" + ); + assert!( + int_section.contains("mov BYTE PTR [r10 + 1], r8b\n"), + "the conversion character must be restored after the length modifier" + ); + + let char_section = &asm[int_end..]; + assert!( + !char_section.starts_with("__rt_sprintf_call_char_linux_x86_64:\n mov BYTE PTR [r10 - 1], 108"), + "%c takes an int in C and must not be widened" + ); + } +} diff --git a/src/codegen_support/runtime/strings/str_to_int.rs b/src/codegen_support/runtime/strings/str_to_int.rs index 7234729066..26e3d5a692 100644 --- a/src/codegen_support/runtime/strings/str_to_int.rs +++ b/src/codegen_support/runtime/strings/str_to_int.rs @@ -91,13 +91,13 @@ fn emit_str_to_int_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rdi, rax"); // strtoll arg1: the C-string pointer emitter.instruction("lea rsi, [rbp - 24]"); // strtoll arg2: &end_i emitter.instruction("mov edx, 10"); // strtoll arg3: parse in base 10 like PHP string-to-int - emitter.instruction("call strtoll"); // rax = integer-form value (LLONG_MAX/MIN on overflow == PHP_INT_MAX/MIN) + emitter.emit_call_c("strtoll"); // rax = integer-form value (LLONG_MAX/MIN on overflow == PHP_INT_MAX/MIN) emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // save the integer-form value // -- float parse: strtod(cstr, &end_d) detects a '.'/'e' float continuation -- emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the C-string pointer for strtod emitter.instruction("lea rsi, [rbp - 32]"); // strtod arg2: &end_d - emitter.instruction("call strtod"); // xmm0 = parsed double value + emitter.emit_call_c("strtod"); // xmm0 = parsed double value // -- choose the integer value unless strtod consumed more bytes (a float part) -- emitter.instruction("mov r8, QWORD PTR [rbp - 32]"); // load the end pointer returned by strtod diff --git a/src/codegen_support/runtime/strings/str_to_number.rs b/src/codegen_support/runtime/strings/str_to_number.rs index eda1dbecc3..1129dbcd32 100644 --- a/src/codegen_support/runtime/strings/str_to_number.rs +++ b/src/codegen_support/runtime/strings/str_to_number.rs @@ -79,7 +79,7 @@ fn emit_str_to_number_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the C-string start pointer for the no-consumption check emitter.instruction("lea rsi, [rbp - 16]"); // pass the address of the local end-pointer slot to strtod emitter.instruction("mov rdi, rax"); // pass the C-string start pointer as strtod's first argument - emitter.instruction("call strtod"); // parse the C string as a double through libc + emitter.emit_call_c("strtod"); // parse the C string as a double through libc emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // load the end pointer returned by strtod emitter.instruction("cmp r8, QWORD PTR [rbp - 8]"); // reject strings where strtod consumed no numeric bytes emitter.instruction("je __rt_str_to_number_false_linux_x86_64"); // no consumed bytes means this is not a numeric string @@ -307,7 +307,7 @@ fn emit_str_looks_like_int_for_coercion_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_sliic_parse_x"); emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the C-string start pointer for strtod emitter.instruction("lea rsi, [rbp - 16]"); // pass the address of the local end-pointer slot to strtod - emitter.instruction("call strtod"); // parse the C string as a double through libc + emitter.emit_call_c("strtod"); // parse the C string as a double through libc emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // load the end pointer returned by strtod emitter.instruction("cmp r8, QWORD PTR [rbp - 8]"); // reject strings where strtod consumed no numeric bytes emitter.instruction("je __rt_sliic_false_x"); // no consumed bytes means this is not coercible to int diff --git a/src/codegen_support/runtime/strings/substr_replace.rs b/src/codegen_support/runtime/strings/substr_replace.rs index a53221adca..39c5df465f 100644 --- a/src/codegen_support/runtime/strings/substr_replace.rs +++ b/src/codegen_support/runtime/strings/substr_replace.rs @@ -53,12 +53,19 @@ pub fn emit_substr_replace(emitter: &mut Emitter) { emitter.instruction("csel x0, x2, x0, gt"); // min(offset, len) // -- compute replace length -- - emitter.instruction("cmn x7, #1"); // check if length == -1 (sentinel) - emitter.instruction("b.ne 2f"); // if not sentinel, use given length - emitter.instruction("sub x7, x2, x0"); // length = remaining from offset + // php reads a negative length as "stop that many bytes before the end of the + // subject", not as "replace nothing". The length is clamped to the available + // bytes here rather than through end = offset + length, because the until-end + // sentinel is now i64::MAX and that sum would overflow. + emitter.instruction("sub x8, x2, x0"); // bytes available from the replacement offset + emitter.instruction("cmp x7, #0"); // is the requested length negative? + emitter.instruction("b.ge 2f"); // a non-negative length only needs the upper clamp + emitter.instruction("add x7, x8, x7"); // stop that many bytes before the end + emitter.instruction("cmp x7, #0"); // did the omission consume the whole window? + emitter.instruction("csel x7, xzr, x7, lt"); // an over-long omission replaces nothing emitter.raw("2:"); - emitter.instruction("cmp x7, #0"); // clamp negative length to 0 - emitter.instruction("csel x7, xzr, x7, lt"); // max(0, length) + emitter.instruction("cmp x7, x8"); // clamp the length to the available bytes + emitter.instruction("csel x7, x8, x7, gt"); // never replace past the end of the subject emitter.instruction("add x8, x0, x7"); // end = offset + length emitter.instruction("cmp x8, x2"); // clamp end to string length emitter.instruction("csel x8, x2, x8, gt"); // min(end, len) @@ -149,14 +156,20 @@ fn emit_substr_replace_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("cmp r9, rcx"); // compare the requested replacement offset against the full subject-string length emitter.instruction("cmovg r9, rcx"); // clamp the replacement offset to the end of the subject string when needed emitter.instruction("mov r10, r8"); // start from the requested replacement length before sentinel and bounds clamping - emitter.instruction("cmp r10, -1"); // check whether the caller omitted the optional replacement length - emitter.instruction("jne __rt_substr_replace_len_known_linux_x86_64"); // skip the sentinel expansion when the caller supplied an explicit replacement length - emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the subject-string length before deriving the tail replacement span - emitter.instruction("sub r10, r9"); // replace the remainder of the subject string when the optional length is omitted + // See the AArch64 arm: a negative length stops that many bytes before the end, + // and the clamp happens against the available bytes rather than through + // end = offset + length, which the i64::MAX sentinel would overflow. + emitter.instruction("mov rcx, QWORD PTR [rbp - 16]"); // reload the subject-string length + emitter.instruction("sub rcx, r9"); // bytes available from the replacement offset + emitter.instruction("cmp r10, 0"); // is the requested replacement length negative? + emitter.instruction("jge __rt_substr_replace_len_known_linux_x86_64"); // a non-negative length only needs the upper clamp + emitter.instruction("add r10, rcx"); // stop that many bytes before the end + emitter.instruction("cmp r10, 0"); // did the omission consume the whole window? + emitter.instruction("jge __rt_substr_replace_len_known_linux_x86_64"); // the window still holds bytes + emitter.instruction("xor r10d, r10d"); // an over-long omission replaces nothing emitter.label("__rt_substr_replace_len_known_linux_x86_64"); - emitter.instruction("cmp r10, 0"); // check whether the requested replacement length is negative - emitter.instruction("mov rcx, 0"); // materialize zero for the negative-length clamp - emitter.instruction("cmovl r10, rcx"); // clamp negative replacement lengths back to zero + emitter.instruction("cmp r10, rcx"); // clamp the length to the available bytes + emitter.instruction("cmovg r10, rcx"); // never replace past the end of the subject emitter.instruction("mov r11, r9"); // seed the suffix start from the clamped replacement offset emitter.instruction("add r11, r10"); // compute the byte offset immediately after the replaced slice emitter.instruction("mov rcx, QWORD PTR [rbp - 16]"); // reload the full subject-string length before clamping the suffix start diff --git a/src/codegen_support/runtime/system/build_argv.rs b/src/codegen_support/runtime/system/build_argv.rs index 80291f87c8..cf040029bb 100644 --- a/src/codegen_support/runtime/system/build_argv.rs +++ b/src/codegen_support/runtime/system/build_argv.rs @@ -16,13 +16,13 @@ use crate::codegen_support::{abi, emit::Emitter, platform::Arch}; /// `ptr+len` slot in a runtime array. Returns the array pointer in the function result /// register (`x0` on ARM64, `rax` on x86_64). /// -/// On ARM64 this function uses `__rt_array_new` and `__rt_array_push_str` for allocation. -/// On x86_64 Linux it calls `malloc` directly with a precomputed layout of `(argc * 16) + 24` -/// bytes to hold the 24-byte array header plus `argc` entries of 16 bytes each (ptr + len). +/// On every target this function uses `__rt_array_new` with 16-byte string slots. That +/// constructor stamps the uniform heap header and the string element type required by +/// dynamic readers such as `eval`; direct `malloc` would leave those metadata words absent. /// Callee-saved registers are preserved across the helper call sequence. pub fn emit_build_argv(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { - emit_build_argv_linux_x86_64(emitter); + emit_build_argv_x86_64(emitter); return; } @@ -96,13 +96,12 @@ pub fn emit_build_argv(emitter: &mut Emitter) { emitter.instruction("ret"); // return to caller } -/// Emits the x86_64 Linux variant of `__rt_build_argv` using the System V AMD64 ABI. -/// Uses callee-saved registers `r8` (argc), `r9` (argv), `r10` (array pointer), `r11` -/// (current string pointer), and `rcx` (loop index). The loop counter lives at `[rbp - 32]` -/// and the array pointer at `[rbp - 24]`. String length is accumulated in `rdx` via null-terminator -/// scan. Allocates with `malloc` using a precomputed size of `(argc * 16) + 24` and stores the -/// resulting array pointer in `rax` before returning. -fn emit_build_argv_linux_x86_64(emitter: &mut Emitter) { +/// Emits the x86_64 variant of `__rt_build_argv` using the internal SysV-shaped ABI. +/// Uses `r8` (argc), `r9` (argv), `r10` (array pointer), `r11` (current string pointer), +/// and `rcx` (loop index). The loop counter lives at `[rbp - 32]` and the array pointer at +/// `[rbp - 24]`. String length is accumulated in `rdx` via null-terminator scan. Allocation +/// goes through `__rt_array_new(argc, 16)` so the result carries its heap and string-slot metadata. +fn emit_build_argv_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: build_argv ---"); emitter.label_global("__rt_build_argv"); @@ -113,19 +112,16 @@ fn emit_build_argv_linux_x86_64(emitter: &mut Emitter) { abi::emit_load_symbol_to_reg(emitter, "r8", "_global_argc", 0); abi::emit_load_symbol_to_reg(emitter, "r9", "_global_argv", 0); - emitter.instruction("mov QWORD PTR [rbp - 8], r8"); // save argc across the malloc call and later loop iterations - emitter.instruction("mov QWORD PTR [rbp - 16], r9"); // save the OS argv pointer array across the malloc call + emitter.instruction("mov QWORD PTR [rbp - 8], r8"); // save argc across the array-constructor call and later loop iterations + emitter.instruction("mov QWORD PTR [rbp - 16], r9"); // save the OS argv pointer array across the array-constructor call - emitter.instruction("mov rdi, r8"); // seed malloc's size argument from argc - emitter.instruction("shl rdi, 4"); // reserve 16 bytes per argv entry for ptr+len storage - emitter.instruction("add rdi, 24"); // include the fixed 24-byte array header in the allocation size - emitter.instruction("call malloc"); // allocate the argv array backing storage with libc malloc + emitter.instruction("mov rdi, r8"); // request one 16-byte string slot for every OS argument + emitter.instruction("mov rsi, 16"); // string payloads use ptr+len slots in indexed arrays + emitter.instruction("call __rt_array_new"); // allocate a stamped indexed string array through the shared heap constructor emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the allocated array pointer for the loop body and final return - emitter.instruction("mov r8, QWORD PTR [rbp - 8]"); // reload argc after malloc may have clobbered caller-saved registers - emitter.instruction("mov QWORD PTR [rax], r8"); // header[0] = logical argv length - emitter.instruction("mov QWORD PTR [rax + 8], r8"); // header[8] = argv capacity - emitter.instruction("mov QWORD PTR [rax + 16], 16"); // header[16] = elem_size for string ptr+len pairs + emitter.instruction("mov r8, QWORD PTR [rbp - 8]"); // reload argc after the constructor clobbered caller-saved registers + emitter.instruction("mov QWORD PTR [rax], r8"); // publish every preallocated argv slot as a logical array element emitter.instruction("mov QWORD PTR [rbp - 32], 0"); // initialize the argv loop counter to zero emitter.label("__rt_build_argv_loop"); @@ -170,18 +166,19 @@ mod tests { use super::*; - /// Verifies that the x86_64 Linux path calls `malloc` for array backing storage and - /// emits the expected header slots (`[rax] = argc`, `[rax + 8] = argc`, `[rax + 16] = 16`) - /// plus string length storage at `[r10 + 8] = rdx`. + /// Verifies that the x86_64 path uses the stamped indexed-array constructor so eval can + /// read `$argv` as `Array`, then writes each measured string length into its slot. #[test] - fn test_emit_build_argv_linux_x86_64_uses_malloc_backing() { - let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); - emit_build_argv(&mut emitter); - let asm = emitter.output(); - - assert!(asm.contains("__rt_build_argv:\n")); - assert!(asm.contains("call malloc\n")); - assert!(asm.contains("mov QWORD PTR [rax + 16], 16\n")); - assert!(asm.contains("mov QWORD PTR [r10 + 8], rdx\n")); + fn test_emit_build_argv_x86_64_uses_stamped_string_array_backing() { + for platform in [Platform::Linux, Platform::Windows] { + let mut emitter = Emitter::new(Target::new(platform, Arch::X86_64)); + emit_build_argv(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains("__rt_build_argv:\n")); + assert!(asm.contains("mov rsi, 16\n")); + assert!(asm.contains("call __rt_array_new\n")); + assert!(asm.contains("mov QWORD PTR [r10 + 8], rdx\n")); + } } } diff --git a/src/codegen_support/runtime/system/date/linux_x86_64.rs b/src/codegen_support/runtime/system/date/linux_x86_64.rs index 24d2ada945..c5e1ee45b6 100644 --- a/src/codegen_support/runtime/system/date/linux_x86_64.rs +++ b/src/codegen_support/runtime/system/date/linux_x86_64.rs @@ -68,7 +68,7 @@ pub(super) fn emit_date_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("cmp rax, -1"); // check whether the builtin requested "current time" instead of an explicit timestamp emitter.instruction("jne __rt_date_have_time_linux_x86_64"); // skip the libc time() query when the caller already supplied an explicit Unix timestamp emitter.instruction("xor edi, edi"); // pass NULL to libc time() so it only returns the current Unix timestamp value - emitter.instruction("call time"); // query libc for the current Unix timestamp when PHP date() was called without an explicit timestamp + emitter.emit_call_c("time"); // query libc for the current Unix timestamp when PHP date() was called without an explicit timestamp emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // store the current Unix timestamp so the rest of the formatter can treat both code paths uniformly emitter.label("__rt_date_have_time_linux_x86_64"); @@ -77,10 +77,10 @@ pub(super) fn emit_date_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // reload the UTC-vs-local decomposition flag emitter.instruction("cmp rax, 0"); // check whether UTC decomposition was requested emitter.instruction("jne __rt_date_use_gmtime_linux_x86_64"); // nonzero flag → decompose as UTC - emitter.instruction("call localtime"); // decompose the Unix timestamp into libc's struct tm fields in the current local timezone + emitter.emit_call_c("localtime"); // decompose the Unix timestamp into libc's struct tm fields in the current local timezone emitter.instruction("jmp __rt_date_decomposed_linux_x86_64"); // skip the UTC decomposition path emitter.label("__rt_date_use_gmtime_linux_x86_64"); - emitter.instruction("call gmtime"); // decompose the Unix timestamp into libc's struct tm fields in UTC + emitter.emit_call_c("gmtime"); // decompose the Unix timestamp into libc's struct tm fields in UTC emitter.label("__rt_date_decomposed_linux_x86_64"); emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save the returned struct tm pointer so each format-token branch can reload the decomposed calendar fields diff --git a/src/codegen_support/runtime/system/date_default_timezone.rs b/src/codegen_support/runtime/system/date_default_timezone.rs index a9751388d0..58269fde64 100644 --- a/src/codegen_support/runtime/system/date_default_timezone.rs +++ b/src/codegen_support/runtime/system/date_default_timezone.rs @@ -153,8 +153,8 @@ fn emit_set_x86_64(emitter: &mut Emitter) { // -- apply via libc and re-read the zone -- emit_symbol_address(emitter, "rdi", "_php_tz_env"); - emitter.instruction("call putenv"); // putenv("TZ=") - emitter.instruction("call tzset"); // re-read TZ so localtime uses it + emitter.emit_call_c("putenv"); // putenv("TZ=") + emitter.emit_call_c("tzset"); // re-read TZ so localtime uses it // -- record the identifier length for date_default_timezone_get and return true -- emit_symbol_address(emitter, "rsi", "_php_default_tz_len"); @@ -271,8 +271,8 @@ fn emit_tz_init_utc_x86_64(emitter: &mut Emitter) { // -- apply via libc -- emit_symbol_address(emitter, "rdi", "_php_tz_env"); - emitter.instruction("call putenv"); // putenv("TZ=UTC") - emitter.instruction("call tzset"); // re-read TZ so localtime resolves UTC + emitter.emit_call_c("putenv"); // putenv("TZ=UTC") + emitter.emit_call_c("tzset"); // re-read TZ so localtime resolves UTC emitter.instruction("add rsp, 16"); // undo the alignment padding emitter.instruction("pop rbp"); // restore caller frame pointer emitter.label("__rt_tz_init_utc_done"); diff --git a/src/codegen_support/runtime/system/getdate.rs b/src/codegen_support/runtime/system/getdate.rs index 2bd4478c78..598deeb0d1 100644 --- a/src/codegen_support/runtime/system/getdate.rs +++ b/src/codegen_support/runtime/system/getdate.rs @@ -183,12 +183,12 @@ pub fn emit_getdate(emitter: &mut Emitter) { emitter.instruction("cmp rax, -1"); // timestamp == -1 (current-time sentinel)? emitter.instruction("jne __rt_getdate_have_x86"); // explicit timestamp supplied → use it emitter.instruction("xor edi, edi"); // NULL argument to time() - emitter.instruction("call time"); // time(NULL) → rax = current Unix timestamp + emitter.emit_call_c("time"); // time(NULL) → rax = current Unix timestamp emitter.label("__rt_getdate_have_x86"); emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the resolved timestamp (also the [0] entry) emitter.instruction("call __rt_tz_init_utc"); // default the timezone to UTC on first use (PHP-compatible) unless already set emitter.instruction("lea rdi, [rbp - 8]"); // rdi = ×tamp for localtime() - emitter.instruction("call localtime"); // localtime(&ts) → rax = struct tm + emitter.emit_call_c("localtime"); // localtime(&ts) → rax = struct tm emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // save the struct tm pointer emitter.instruction("mov rdi, 16"); // initial capacity 16 (>= 11 entries, avoids a mid-build realloc) emitter.instruction("mov rsi, 7"); // value type = mixed diff --git a/src/codegen_support/runtime/system/getenv.rs b/src/codegen_support/runtime/system/getenv.rs index 9d8fbe058c..31141d013e 100644 --- a/src/codegen_support/runtime/system/getenv.rs +++ b/src/codegen_support/runtime/system/getenv.rs @@ -78,7 +78,7 @@ fn emit_getenv_linux_x86_64(emitter: &mut Emitter) { abi::emit_call_label(emitter, "__rt_cstr"); // convert the elephc string result regs into a null-terminated C string in the scratch buffer emitter.instruction("mov rdi, rax"); // pass the null-terminated environment variable name in the SysV first-argument register - emitter.bl_c("getenv"); // getenv(name) → rax=value ptr or NULL + emitter.emit_call_c("getenv"); // getenv(name) → rax=value ptr or NULL emitter.instruction("test rax, rax"); // did libc return a real environment-value pointer? emitter.instruction("je __rt_getenv_empty"); // missing environment variables map to the empty PHP string diff --git a/src/codegen_support/runtime/system/json_decode_mixed/arrays.rs b/src/codegen_support/runtime/system/json_decode_mixed/arrays.rs index 1e3c5d509b..8e00d935f7 100644 --- a/src/codegen_support/runtime/system/json_decode_mixed/arrays.rs +++ b/src/codegen_support/runtime/system/json_decode_mixed/arrays.rs @@ -267,7 +267,7 @@ pub(super) fn emit_x86_64(emitter: &mut Emitter) { // [rbp - 48] = after_comma flag emitter.instruction("push rbp"); // save the caller frame pointer emitter.instruction("mov rbp, rsp"); // establish the helper frame - emitter.instruction("sub rsp, 48"); // reserve aligned scratch + emitter.instruction("sub rsp, 48"); // reserve aligned decoder scratch emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // park slice_ptr emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // park slice_len @@ -306,18 +306,17 @@ pub(super) fn emit_x86_64(emitter: &mut Emitter) { // rax = slice_ptr (kept reloadable but we use [rbp-8] when needed) // r10 = depth // r11 = in_string flag - // r12 = escape flag (callee-saved — we save before clobbering) + // r9 = escape flag (volatile and call-free within the scanner) // r8 = current byte emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // restore slice_len after the whitespace helper used an exclusive limit - emitter.instruction("push r12"); // preserve callee-saved (System V keeps r12) emitter.instruction("xor r10, r10"); // depth emitter.instruction("xor r11, r11"); // in_string - emitter.instruction("xor r12, r12"); // escape + emitter.instruction("xor r9, r9"); // clear the escape flag emitter.label("__rt_json_decode_array_real_scan_x"); emitter.instruction("cmp rcx, rdx"); // hit slice end? emitter.instruction("jge __rt_json_decode_array_real_scan_done_x"); // branch on the current JSON decoder condition emitter.instruction("movzx r8, BYTE PTR [rax + rcx]"); // load or prepare JSON decoder state - emitter.instruction("test r12, r12"); // check the current JSON decoder condition + emitter.instruction("test r9, r9"); // check the current JSON decoder condition emitter.instruction("jne __rt_json_decode_array_real_scan_after_escape_x"); // branch on the current JSON decoder condition emitter.instruction("test r11, r11"); // check the current JSON decoder condition emitter.instruction("jne __rt_json_decode_array_real_scan_in_string_x"); // branch on the current JSON decoder condition @@ -356,15 +355,14 @@ pub(super) fn emit_x86_64(emitter: &mut Emitter) { emitter.instruction("xor r11, r11"); // update the JSON decoder cursor or counter emitter.instruction("jmp __rt_json_decode_array_real_scan_advance_x"); // continue in the JSON decoder control path emitter.label("__rt_json_decode_array_real_scan_set_escape_x"); - emitter.instruction("mov r12, 1"); // load or prepare JSON decoder state + emitter.instruction("mov r9, 1"); // mark the next byte as escaped emitter.instruction("jmp __rt_json_decode_array_real_scan_advance_x"); // continue in the JSON decoder control path emitter.label("__rt_json_decode_array_real_scan_after_escape_x"); - emitter.instruction("xor r12, r12"); // update the JSON decoder cursor or counter + emitter.instruction("xor r9, r9"); // clear the consumed escape flag emitter.label("__rt_json_decode_array_real_scan_advance_x"); emitter.instruction("add rcx, 1"); // update the JSON decoder cursor or counter emitter.instruction("jmp __rt_json_decode_array_real_scan_x"); // continue in the JSON decoder control path emitter.label("__rt_json_decode_array_real_scan_done_x"); - emitter.instruction("pop r12"); // restore callee-saved register emitter.instruction("mov QWORD PTR [rbp - 24], rcx"); // cursor at separator // Recursively decode the element sub-slice. diff --git a/src/codegen_support/runtime/system/json_decode_mixed/objects.rs b/src/codegen_support/runtime/system/json_decode_mixed/objects.rs index 7da1629da2..3622f42494 100644 --- a/src/codegen_support/runtime/system/json_decode_mixed/objects.rs +++ b/src/codegen_support/runtime/system/json_decode_mixed/objects.rs @@ -318,7 +318,7 @@ pub(super) fn emit_x86_64(emitter: &mut Emitter) { // [rbp - 64] = after_comma flag emitter.instruction("push rbp"); // preserve or restore JSON decoder scratch state emitter.instruction("mov rbp, rsp"); // load or prepare JSON decoder state - emitter.instruction("sub rsp, 64"); // update the JSON decoder cursor or counter + emitter.instruction("sub rsp, 64"); // reserve aligned decoder scratch emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // load or prepare JSON decoder state emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // load or prepare JSON decoder state @@ -353,13 +353,12 @@ pub(super) fn emit_x86_64(emitter: &mut Emitter) { // Save key_start, scan to closing `"`. emitter.instruction("mov QWORD PTR [rbp - 40], rcx"); // load or prepare JSON decoder state emitter.instruction("add rcx, 1"); // step past opening `"` - emitter.instruction("push r12"); // preserve callee-saved - emitter.instruction("xor r12, r12"); // escape flag + emitter.instruction("xor r9, r9"); // clear the key escape flag emitter.label("__rt_json_decode_object_real_key_scan_x"); emitter.instruction("cmp rcx, rdx"); // check the current JSON decoder condition emitter.instruction("jge __rt_json_decode_object_real_key_fail_x"); // branch on the current JSON decoder condition emitter.instruction("movzx r10, BYTE PTR [rax + rcx]"); // load or prepare JSON decoder state - emitter.instruction("test r12, r12"); // check the current JSON decoder condition + emitter.instruction("test r9, r9"); // check the current JSON decoder condition emitter.instruction("jne __rt_json_decode_object_real_key_after_escape_x"); // branch on the current JSON decoder condition emitter.instruction("cmp r10, 92"); // '\\' emitter.instruction("je __rt_json_decode_object_real_key_set_escape_x"); // branch on the current JSON decoder condition @@ -367,15 +366,14 @@ pub(super) fn emit_x86_64(emitter: &mut Emitter) { emitter.instruction("je __rt_json_decode_object_real_key_done_x"); // branch on the current JSON decoder condition emitter.instruction("jmp __rt_json_decode_object_real_key_advance_x"); // continue in the JSON decoder control path emitter.label("__rt_json_decode_object_real_key_set_escape_x"); - emitter.instruction("mov r12, 1"); // load or prepare JSON decoder state + emitter.instruction("mov r9, 1"); // mark the next key byte as escaped emitter.instruction("jmp __rt_json_decode_object_real_key_advance_x"); // continue in the JSON decoder control path emitter.label("__rt_json_decode_object_real_key_after_escape_x"); - emitter.instruction("xor r12, r12"); // update the JSON decoder cursor or counter + emitter.instruction("xor r9, r9"); // clear the consumed key escape flag emitter.label("__rt_json_decode_object_real_key_advance_x"); emitter.instruction("add rcx, 1"); // update the JSON decoder cursor or counter emitter.instruction("jmp __rt_json_decode_object_real_key_scan_x"); // continue in the JSON decoder control path emitter.label("__rt_json_decode_object_real_key_done_x"); - emitter.instruction("pop r12"); // preserve or restore JSON decoder scratch state emitter.instruction("add rcx, 1"); // include the closing `"` emitter.instruction("mov QWORD PTR [rbp - 24], rcx"); // load or prepare JSON decoder state @@ -412,15 +410,14 @@ pub(super) fn emit_x86_64(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [rbp - 56], rcx"); // load or prepare JSON decoder state // Boundary scanner for the value. - emitter.instruction("push r12"); // preserve or restore JSON decoder scratch state emitter.instruction("xor r10, r10"); // depth emitter.instruction("xor r11, r11"); // in_string - emitter.instruction("xor r12, r12"); // escape + emitter.instruction("xor r9, r9"); // clear the value escape flag emitter.label("__rt_json_decode_object_real_value_scan_x"); emitter.instruction("cmp rcx, rdx"); // check the current JSON decoder condition emitter.instruction("jge __rt_json_decode_object_real_value_done_x"); // branch on the current JSON decoder condition emitter.instruction("movzx r8, BYTE PTR [rax + rcx]"); // load or prepare JSON decoder state - emitter.instruction("test r12, r12"); // check the current JSON decoder condition + emitter.instruction("test r9, r9"); // check the current JSON decoder condition emitter.instruction("jne __rt_json_decode_object_real_value_after_escape_x"); // branch on the current JSON decoder condition emitter.instruction("test r11, r11"); // check the current JSON decoder condition emitter.instruction("jne __rt_json_decode_object_real_value_in_string_x"); // branch on the current JSON decoder condition @@ -458,15 +455,14 @@ pub(super) fn emit_x86_64(emitter: &mut Emitter) { emitter.instruction("xor r11, r11"); // update the JSON decoder cursor or counter emitter.instruction("jmp __rt_json_decode_object_real_value_advance_x"); // continue in the JSON decoder control path emitter.label("__rt_json_decode_object_real_value_set_escape_x"); - emitter.instruction("mov r12, 1"); // load or prepare JSON decoder state + emitter.instruction("mov r9, 1"); // mark the next value byte as escaped emitter.instruction("jmp __rt_json_decode_object_real_value_advance_x"); // continue in the JSON decoder control path emitter.label("__rt_json_decode_object_real_value_after_escape_x"); - emitter.instruction("xor r12, r12"); // update the JSON decoder cursor or counter + emitter.instruction("xor r9, r9"); // clear the consumed value escape flag emitter.label("__rt_json_decode_object_real_value_advance_x"); emitter.instruction("add rcx, 1"); // update the JSON decoder cursor or counter emitter.instruction("jmp __rt_json_decode_object_real_value_scan_x"); // continue in the JSON decoder control path emitter.label("__rt_json_decode_object_real_value_done_x"); - emitter.instruction("pop r12"); // preserve or restore JSON decoder scratch state emitter.instruction("mov QWORD PTR [rbp - 24], rcx"); // load or prepare JSON decoder state // Recursively decode value sub-slice. @@ -551,7 +547,6 @@ pub(super) fn emit_x86_64(emitter: &mut Emitter) { emitter.instruction("ret"); // return Mixed* (stdClass) in rax emitter.label("__rt_json_decode_object_real_key_fail_x"); - emitter.instruction("pop r12"); // preserve or restore JSON decoder scratch state emitter.label("__rt_json_decode_object_real_fail_x"); emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // load the object slice pointer for location reporting emitter.instruction("add rax, QWORD PTR [rbp - 24]"); // convert the object-local cursor to an absolute source pointer diff --git a/src/codegen_support/runtime/system/json_decode_mixed/x86_64.rs b/src/codegen_support/runtime/system/json_decode_mixed/x86_64.rs index 1767a8b6e0..5f911b497c 100644 --- a/src/codegen_support/runtime/system/json_decode_mixed/x86_64.rs +++ b/src/codegen_support/runtime/system/json_decode_mixed/x86_64.rs @@ -452,7 +452,7 @@ pub(super) fn emit(emitter: &mut Emitter) { emitter.label("__rt_json_decode_mixed_float_copy_done"); emitter.instruction("mov BYTE PTR [r10 + rcx], 0"); // append the NUL terminator atof needs emitter.instruction("mov rdi, r10"); // pass the C-string pointer to atof in rdi - emitter.bl_c("atof"); // libc atof → xmm0 = double + emitter.emit_call_c("atof"); // libc atof → xmm0 = double emitter.instruction("movq rdi, xmm0"); // move the double bits into the integer payload register emitter.instruction("mov rax, 2"); // tag = float emitter.instruction("xor rsi, rsi"); // update the JSON decoder cursor or counter diff --git a/src/codegen_support/runtime/system/json_encode_object.rs b/src/codegen_support/runtime/system/json_encode_object.rs index 6e3c3162f8..74880e236a 100644 --- a/src/codegen_support/runtime/system/json_encode_object.rs +++ b/src/codegen_support/runtime/system/json_encode_object.rs @@ -490,7 +490,7 @@ fn emit_json_encode_object_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_json_obj_jsonserialize_invoke_x"); emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // restore the receiver object pointer as the SysV $this argument emitter.instruction("mov rdx, QWORD PTR [rbp - 56]"); // reload the saved jsonSerialize method target - emitter.instruction("call rdx"); // invoke jsonSerialize on the receiver and capture the boxed mixed result + emitter.emit_platform_callback_call("rdx", 1); emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // park the boxed mixed return value across the prefix-restore loop // Copy the heap-owned prefix back into concat_buf[0..prefix_len]. emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the saved prefix length diff --git a/src/codegen_support/runtime/system/json_ftoa.rs b/src/codegen_support/runtime/system/json_ftoa.rs index dae828f7da..2be5b17e9a 100644 --- a/src/codegen_support/runtime/system/json_ftoa.rs +++ b/src/codegen_support/runtime/system/json_ftoa.rs @@ -245,10 +245,10 @@ fn emit_x86_64(emitter: &mut Emitter) { emitter.instruction("mov ecx, ebx"); // variadic int precision = p emitter.instruction("movsd xmm0, QWORD PTR [rsp + 64]"); // variadic double = input value emitter.instruction("mov eax, 1"); // one vector register used by the variadic call - emitter.instruction("call snprintf"); // format x at precision p into scratch + emitter.emit_call_c("snprintf"); // format x at precision p into scratch emitter.instruction("lea rdi, [rsp]"); // strtod source = formatted scratch emitter.instruction("xor esi, esi"); // strtod endptr = NULL - emitter.instruction("call strtod"); // parse the formatted string back to a double (xmm0) + emitter.emit_call_c("strtod"); // parse the formatted string back to a double (xmm0) emitter.instruction("movsd xmm1, QWORD PTR [rsp + 64]"); // reload the original input double emitter.instruction("ucomisd xmm0, xmm1"); // did the formatted string round-trip exactly? emitter.instruction("je __rt_json_ftoa_probe_done_x"); // shortest precision found @@ -271,7 +271,7 @@ fn emit_x86_64(emitter: &mut Emitter) { emitter.instruction("lea rdi, [rsp + rax + 1]"); // address of the exponent text after 'e' emitter.instruction("xor esi, esi"); // strtol endptr = NULL emitter.instruction("mov edx, 10"); // base 10 - emitter.instruction("call strtol"); // E = parsed decimal exponent + emitter.emit_call_c("strtol"); // E = parsed decimal exponent emitter.instruction("mov r13, rax"); // keep E in a callee-saved register emitter.instruction("lea rax, [r13 + 1]"); // decpt = E + 1 @@ -293,7 +293,7 @@ fn emit_x86_64(emitter: &mut Emitter) { abi::emit_symbol_address(emitter, "rdx", "_fmt_star_f"); emitter.instruction("movsd xmm0, QWORD PTR [rsp + 64]"); // variadic double = input value emitter.instruction("mov eax, 1"); // one vector register used by the variadic call - emitter.instruction("call snprintf"); // format the decimal digits into concat_buf + emitter.emit_call_c("snprintf"); // format the decimal digits into concat_buf emitter.instruction("mov rdx, rax"); // result length = bytes written abi::emit_load_symbol_to_reg(emitter, "r8", "_concat_off", 0); // original offset (unchanged by snprintf) abi::emit_symbol_address(emitter, "r9", "_concat_buf"); diff --git a/src/codegen_support/runtime/system/localtime.rs b/src/codegen_support/runtime/system/localtime.rs index 4b31e39ae4..615a63dca9 100644 --- a/src/codegen_support/runtime/system/localtime.rs +++ b/src/codegen_support/runtime/system/localtime.rs @@ -213,12 +213,12 @@ pub fn emit_localtime(emitter: &mut Emitter) { emitter.instruction("cmp rax, -1"); // timestamp == -1 (current-time sentinel)? emitter.instruction("jne __rt_localtime_have_x86"); // explicit timestamp supplied → use it emitter.instruction("xor edi, edi"); // NULL argument to time() - emitter.instruction("call time"); // time(NULL) → rax = current Unix timestamp + emitter.emit_call_c("time"); // time(NULL) → rax = current Unix timestamp emitter.label("__rt_localtime_have_x86"); emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the resolved timestamp emitter.instruction("call __rt_tz_init_utc"); // default the timezone to UTC on first use (PHP-compatible) unless already set emitter.instruction("lea rdi, [rbp - 8]"); // rdi = ×tamp for localtime() - emitter.instruction("call localtime"); // localtime(&ts) → rax = struct tm + emitter.emit_call_c("localtime"); // localtime(&ts) → rax = struct tm emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // save the struct tm pointer emitter.instruction("mov rdi, 16"); // capacity 16 (>= 9 entries, avoids a realloc) emitter.instruction("mov rsi, 7"); // value type = mixed diff --git a/src/codegen_support/runtime/system/mb_ereg_match.rs b/src/codegen_support/runtime/system/mb_ereg_match.rs index 6622e6cf14..bb433548f7 100644 --- a/src/codegen_support/runtime/system/mb_ereg_match.rs +++ b/src/codegen_support/runtime/system/mb_ereg_match.rs @@ -194,7 +194,7 @@ fn emit_mb_ereg_match_linux_x86_64(emitter: &mut Emitter) { // -- pcre2_regcomp(®ex_t, pattern, flags) -- emitter.instruction("lea rdi, [rsp]"); // pass local regex_t storage as regcomp argument 1 emitter.instruction(&format!("mov rsi, QWORD PTR [rsp + {}]", pattern_cstr_off)); // pass pattern C string as regcomp argument 2 - emitter.bl_c("pcre2_regcomp"); // compile regex through PCRE2 + emitter.emit_call_c("pcre2_regcomp"); // compile regex through PCRE2 emitter.instruction("test eax, eax"); // did regex compilation succeed? emitter.instruction("jnz __rt_mb_ereg_match_no_x86"); // compile failure produces no match @@ -210,12 +210,12 @@ fn emit_mb_ereg_match_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov edx, 1"); // request one regmatch slot for rm_so emitter.instruction(&format!("lea rcx, [rsp + {}]", regmatch_off)); // pass local regmatch_t output buffer emitter.instruction("xor r8d, r8d"); // use default regexec flags - emitter.bl_c("pcre2_regexec"); // execute regex through PCRE2 + emitter.emit_call_c("pcre2_regexec"); // execute regex through PCRE2 emitter.instruction(&format!("mov DWORD PTR [rsp + {}], eax", regexec_result_off)); // save regexec status across cleanup // -- free compiled regex -- emitter.instruction("lea rdi, [rsp]"); // reload compiled regex_t storage for regfree - emitter.bl_c("pcre2_regfree"); // release compiled regex resources + emitter.emit_call_c("pcre2_regfree"); // release compiled regex resources // -- no match if regexec != 0 -- emitter.instruction(&format!("mov eax, DWORD PTR [rsp + {}]", regexec_result_off)); // reload regexec status after regfree diff --git a/src/codegen_support/runtime/system/microtime.rs b/src/codegen_support/runtime/system/microtime.rs index 4744584824..68c9472e72 100644 --- a/src/codegen_support/runtime/system/microtime.rs +++ b/src/codegen_support/runtime/system/microtime.rs @@ -9,7 +9,7 @@ //! - System helpers must preserve PHP-visible behavior while crossing libc, syscall, JSON, regex, and date formatter boundaries. use crate::codegen_support::emit::Emitter; -use crate::codegen_support::platform::Arch; +use crate::codegen_support::platform::{Arch, Platform}; /// __rt_microtime: get current time as float seconds via gettimeofday syscall. /// Output: d0 = seconds.microseconds as float @@ -31,11 +31,28 @@ pub(crate) fn emit_microtime(emitter: &mut Emitter) { // -- call gettimeofday syscall -- emitter.instruction("add x0, sp, #0"); // x0 = pointer to timeval struct on stack emitter.instruction("mov x1, #0"); // x1 = NULL (timezone not needed) - emitter.syscall(116); - - // -- extract tv_sec and tv_usec -- - emitter.instruction("ldr x0, [sp, #0]"); // x0 = tv_sec (seconds) - emitter.instruction("ldr x1, [sp, #8]"); // x1 = tv_usec (microseconds) + // Darwin's gettimeofday trap returns 0 and does not fill the caller's timeval -- + // populating it is libsystem's job, through the commpage, not the kernel's. The + // raw trap therefore left the struct holding whatever the stack carried at that + // depth, so microtime(true) returned around 3.67e12 (a microsecond uptime counter + // left by an earlier runtime call) rather than an epoch value -- and looked + // correct in any program where a preceding libc call had left a real timeval in + // those bytes, which is why it read as intermittent. Going through libc gets the + // documented behaviour, and matches what the x86_64 arm already does. Linux fills + // the struct from the trap, so that arm keeps it. + if emitter.platform == Platform::Linux { + emitter.syscall(116); + emitter.instruction("ldr x0, [sp, #0]"); // x0 = tv_sec (seconds) + emitter.instruction("ldr x1, [sp, #8]"); // x1 = tv_usec (microseconds) + } else { + emitter.bl_c("gettimeofday"); // let libsystem fill the timeval it owns + emitter.instruction("ldr x0, [sp, #0]"); // x0 = tv_sec (seconds) + // Darwin's suseconds_t is 32 bits, so tv_usec occupies four bytes and the + // next four are padding libc does not clear. A 64-bit load folded that + // padding into the microseconds: whenever it held 1, the result landed + // 2**32 microseconds -- 4294.967296 seconds -- past the real time. + emitter.instruction("ldr w1, [sp, #8]"); // x1 = tv_usec, zero-extended from its 32-bit field + } // -- convert to float: d0 = tv_sec + tv_usec / 1000000.0 -- emitter.instruction("scvtf d0, x0"); // d0 = (double)tv_sec @@ -65,7 +82,7 @@ fn emit_microtime_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("sub rsp, 32"); // reserve aligned stack storage for one timeval struct plus scratch padding before the libc call emitter.instruction("lea rdi, [rsp]"); // pass the temporary timeval storage as the first SysV integer argument to libc gettimeofday() emitter.instruction("xor esi, esi"); // pass NULL as the timezone pointer because elephc only needs the current Unix timestamp - emitter.bl_c("gettimeofday"); // fill the temporary timeval with the current wall-clock time through libc + emitter.emit_call_c("gettimeofday"); // fill the temporary timeval with the current wall-clock time through libc emitter.instruction("cvtsi2sd xmm0, QWORD PTR [rsp]"); // convert tv_sec from the temporary timeval into the base double-precision second count emitter.instruction("cvtsi2sd xmm1, QWORD PTR [rsp + 8]"); // convert tv_usec from the temporary timeval into a double-precision microsecond count emitter.instruction("mov r10, 1000000"); // materialize the number of microseconds per second before converting it into a floating divisor @@ -187,7 +204,7 @@ fn emit_microtime_build_into_linux_x86_64(emitter: &mut Emitter) { // -- call gettimeofday -- emitter.instruction("lea rdi, [rsp]"); // rdi = pointer to the timeval storage emitter.instruction("xor esi, esi"); // rsi = NULL (timezone not needed) - emitter.bl_c("gettimeofday"); // fill the timeval with the current wall-clock time + emitter.emit_call_c("gettimeofday"); // fill the timeval with the current wall-clock time // -- reload the buffer and write the "0." prefix -- emitter.instruction("mov rdi, QWORD PTR [rsp + 16]"); // rdi = destination buffer (reloaded after the libc call) diff --git a/src/codegen_support/runtime/system/mktime.rs b/src/codegen_support/runtime/system/mktime.rs index f463d9b471..cc7f42b477 100644 --- a/src/codegen_support/runtime/system/mktime.rs +++ b/src/codegen_support/runtime/system/mktime.rs @@ -123,7 +123,7 @@ fn emit_mktime_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("call __rt_tz_init_utc"); // default the timezone to UTC on first use (PHP-compatible) unless already set emitter.instruction("mov rdi, rsp"); // pass the temporary struct tm as the first SysV integer argument to libc mktime() - emitter.instruction("call mktime"); // ask libc to convert the PHP date/time components into a Unix timestamp + emitter.emit_call_c("mktime"); // ask libc to convert the PHP date/time components into a Unix timestamp emit_pre1900_shift_epilogue(emitter); @@ -357,7 +357,7 @@ pub fn emit_mktime_shifted(emitter: &mut Emitter) { emitter.label("__rt_mkts_skip_x86"); emitter.instruction("mov QWORD PTR [rsp + 16], r10"); // save the cycle count across the libc call emitter.instruction("mov rdi, QWORD PTR [rsp + 0]"); // rdi = struct tm pointer - emitter.bl_c("mktime"); // call libc mktime on the (possibly shifted) struct tm + emitter.emit_call_c("mktime"); // call libc mktime on the (possibly shifted) struct tm emitter.instruction("mov rcx, QWORD PTR [rsp + 0]"); // reload the struct tm pointer emitter.instruction("mov edx, DWORD PTR [rcx + 20]"); // mktime normalized tm_year for the shifted year emitter.instruction("mov r10, QWORD PTR [rsp + 16]"); // reload the cycle count diff --git a/src/codegen_support/runtime/system/php_uname.rs b/src/codegen_support/runtime/system/php_uname.rs index 30f57e4688..26365bfbc0 100644 --- a/src/codegen_support/runtime/system/php_uname.rs +++ b/src/codegen_support/runtime/system/php_uname.rs @@ -34,7 +34,7 @@ fn uts_field_len(platform: Platform) -> usize { match platform { Platform::MacOS => 256, Platform::Linux => 65, - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => 65, // msvcrt utsname uses 65-byte fields, same as Linux } } diff --git a/src/codegen_support/runtime/system/preg_match.rs b/src/codegen_support/runtime/system/preg_match.rs index 97c0a251e2..2060c42ab8 100644 --- a/src/codegen_support/runtime/system/preg_match.rs +++ b/src/codegen_support/runtime/system/preg_match.rs @@ -361,7 +361,7 @@ fn emit_preg_match_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("lea rdi, [rsp]"); // pass the local regex_t storage as the first regcomp() argument emitter.instruction(&format!("mov rsi, QWORD PTR [rsp + {}]", pattern_cstr_off)); // pass the null-terminated PCRE pattern C string as the second regcomp() argument emitter.instruction(&format!("mov edx, DWORD PTR [rsp + {}]", flags_off)); // pass PCRE2 POSIX compile flags from delimiter parsing - emitter.bl_c("pcre2_regcomp"); // compile the PCRE pattern into the local regex_t storage + emitter.emit_call_c("pcre2_regcomp"); // compile the PCRE pattern into the local regex_t storage emitter.instruction("test eax, eax"); // did regcomp() succeed and produce a compiled regex object? emitter.instruction("jnz __rt_preg_match_no_linux_x86_64"); // failed regex compilation maps to a PHP false-style no-match result emitter.instruction(&format!("mov rax, QWORD PTR [rsp + {}]", subject_ptr_off)); // reload the elephc subject pointer before null-terminating it in the secondary scratch buffer @@ -373,10 +373,10 @@ fn emit_preg_match_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov edx, 1"); // request exactly one regmatch_t capture because preg_match() only needs match/no-match emitter.instruction(&format!("lea rcx, [rsp + {}]", regmatch_off)); // pass the local regmatch_t buffer as the match output slot for regexec() emitter.instruction("xor r8d, r8d"); // pass eflags = 0 so PCRE2 regex execution performs a normal match from the subject start - emitter.bl_c("pcre2_regexec"); // execute the compiled PCRE2 regex against the null-terminated subject string + emitter.emit_call_c("pcre2_regexec"); // execute the compiled PCRE2 regex against the null-terminated subject string emitter.instruction(&format!("mov DWORD PTR [rsp + {}], eax", regexec_result_off)); // preserve the regexec() result code across the mandatory regfree() cleanup call emitter.instruction("lea rdi, [rsp]"); // reload the compiled regex_t storage before freeing it with regfree() - emitter.bl_c("pcre2_regfree"); // release any internal PCRE2 regex resources held by the local regex_t object + emitter.emit_call_c("pcre2_regfree"); // release any internal PCRE2 regex resources held by the local regex_t object emitter.instruction(&format!("mov eax, DWORD PTR [rsp + {}]", regexec_result_off)); // reload the saved regexec() status code after regfree() clobbered caller-saved registers emitter.instruction("test eax, eax"); // interpret a zero regexec() result as a successful regex match emitter.instruction("jnz __rt_preg_match_no_linux_x86_64"); // return zero when PCRE2 regex execution reports no match @@ -430,7 +430,7 @@ fn emit_preg_match_capture_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("lea rdi, [rsp]"); // pass local regex_t storage to regcomp emitter.instruction(&format!("mov rsi, QWORD PTR [rsp + {}]", pattern_cstr_off)); // pass null-terminated PCRE pattern to regcomp emitter.instruction(&format!("mov edx, DWORD PTR [rsp + {}]", flags_off)); // pass PCRE2 POSIX compile flags from delimiter parsing - emitter.bl_c("pcre2_regcomp"); // compile regex through PCRE2 + emitter.emit_call_c("pcre2_regcomp"); // compile regex through PCRE2 emitter.instruction("test eax, eax"); // did regex compilation succeed? emitter.instruction("jnz __rt_preg_match_capture_empty_linux_x86_64"); // compile failure returns no match and an empty matches array emitter.instruction(&format!("mov r9, QWORD PTR [rsp + {}]", regex_re_nsub_off)); // load regex_t.re_nsub after successful compilation @@ -442,7 +442,7 @@ fn emit_preg_match_capture_linux_x86_64(emitter: &mut Emitter) { } else { emitter.instruction("shl rdi, 3"); // malloc bytes = nmatch * 8-byte regmatch_t slots } - emitter.bl_c("malloc"); // allocate the regmatch_t vector for all capture groups + emitter.emit_call_c("malloc"); // allocate the regmatch_t vector for all capture groups emitter.instruction("test rax, rax"); // did malloc return a capture buffer? emitter.instruction("jz __rt_preg_match_capture_malloc_fail_linux_x86_64"); // allocation failure frees regex_t and returns no match emitter.instruction(&format!("mov QWORD PTR [rsp + {}], rax", regmatches_ptr_off)); // save dynamic regmatch_t buffer pointer @@ -456,10 +456,10 @@ fn emit_preg_match_capture_linux_x86_64(emitter: &mut Emitter) { emitter.instruction(&format!("mov rdx, QWORD PTR [rsp + {}]", nmatch_off)); // request one regmatch slot for every capture group emitter.instruction(&format!("mov rcx, QWORD PTR [rsp + {}]", regmatches_ptr_off)); // pass dynamic regmatch_t capture buffer emitter.instruction("xor r8d, r8d"); // use default regexec execution flags - emitter.bl_c("pcre2_regexec"); // execute regex and fill capture offsets + emitter.emit_call_c("pcre2_regexec"); // execute regex and fill capture offsets emitter.instruction(&format!("mov DWORD PTR [rsp + {}], eax", regexec_result_off)); // save regexec status across regfree emitter.instruction("lea rdi, [rsp]"); // reload regex_t storage for regfree - emitter.bl_c("pcre2_regfree"); // release compiled regex resources + emitter.emit_call_c("pcre2_regfree"); // release compiled regex resources emitter.instruction(&format!("mov eax, DWORD PTR [rsp + {}]", regexec_result_off)); // reload saved regexec status emitter.instruction("test eax, eax"); // was there a successful regex match? emitter.instruction("jnz __rt_preg_match_capture_no_match_linux_x86_64"); // no match frees capture storage and returns an empty array @@ -524,19 +524,19 @@ fn emit_preg_match_capture_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_preg_match_capture_success_linux_x86_64"); emitter.instruction(&format!("mov rdi, QWORD PTR [rsp + {}]", regmatches_ptr_off)); // reload dynamic capture buffer for cleanup - emitter.bl_c("free"); // free the dynamic regmatch_t vector before returning matches + emitter.emit_call_c("free"); // free the dynamic regmatch_t vector before returning matches emitter.instruction(&format!("mov rdx, QWORD PTR [rsp + {}]", matches_array_off)); // return matches array pointer in rdx emitter.instruction("mov eax, 1"); // report that preg_match found a match emitter.instruction("jmp __rt_preg_match_capture_ret_linux_x86_64"); // share helper epilogue emitter.label("__rt_preg_match_capture_no_match_linux_x86_64"); emitter.instruction(&format!("mov rdi, QWORD PTR [rsp + {}]", regmatches_ptr_off)); // reload dynamic capture buffer for the no-match cleanup path - emitter.bl_c("free"); // free the dynamic regmatch_t vector before returning an empty matches array + emitter.emit_call_c("free"); // free the dynamic regmatch_t vector before returning an empty matches array emitter.instruction("jmp __rt_preg_match_capture_empty_linux_x86_64"); // allocate and return the empty matches array emitter.label("__rt_preg_match_capture_malloc_fail_linux_x86_64"); emitter.instruction("lea rdi, [rsp]"); // reload regex_t storage after capture-buffer allocation failed - emitter.bl_c("pcre2_regfree"); // free compiled regex resources before returning no match + emitter.emit_call_c("pcre2_regfree"); // free compiled regex resources before returning no match emitter.label("__rt_preg_match_capture_empty_linux_x86_64"); emitter.instruction("xor edi, edi"); // empty array capacity for no-match or compile-failure paths diff --git a/src/codegen_support/runtime/system/preg_match_all.rs b/src/codegen_support/runtime/system/preg_match_all.rs index da3c9a2cce..6d81ba5458 100644 --- a/src/codegen_support/runtime/system/preg_match_all.rs +++ b/src/codegen_support/runtime/system/preg_match_all.rs @@ -160,7 +160,7 @@ fn emit_preg_match_all_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("lea rdi, [rsp]"); // pass the local regex_t storage as the first regcomp() argument emitter.instruction(&format!("mov rsi, QWORD PTR [rsp + {}]", pattern_cstr_off)); // pass the null-terminated PCRE pattern C string as the second regcomp() argument emitter.instruction(&format!("mov edx, DWORD PTR [rsp + {}]", flags_off)); // pass PCRE2 POSIX compile flags from delimiter parsing - emitter.bl_c("pcre2_regcomp"); // compile the PCRE pattern into the local regex_t storage + emitter.emit_call_c("pcre2_regcomp"); // compile the PCRE pattern into the local regex_t storage emitter.instruction("test eax, eax"); // did regcomp() succeed and produce a compiled regex object? emitter.instruction("jnz __rt_preg_match_all_fail_linux_x86_64"); // failed regex compilation maps to a zero-count result emitter.instruction(&format!("mov rax, QWORD PTR [rsp + {}]", subject_ptr_off)); // reload the elephc subject pointer before null-terminating it in the secondary scratch buffer @@ -179,7 +179,7 @@ fn emit_preg_match_all_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov edx, 1"); // request exactly one regmatch_t capture because the loop only needs the overall match extent emitter.instruction(&format!("lea rcx, [rsp + {}]", regmatch_off)); // pass the local regmatch_t buffer as the match extent output slot emitter.instruction("xor r8d, r8d"); // pass eflags = 0 so regexec() matches from the current subject cursor - emitter.bl_c("pcre2_regexec"); // execute the compiled PCRE2 regex at the current subject cursor + emitter.emit_call_c("pcre2_regexec"); // execute the compiled PCRE2 regex at the current subject cursor emitter.instruction("test eax, eax"); // did regexec() find another match at or after the current cursor? emitter.instruction("jnz __rt_preg_match_all_done_linux_x86_64"); // stop counting when regexec() reports no further matches emitter.instruction(&format!("mov r9, QWORD PTR [rsp + {}]", match_count_off)); // reload the running non-overlapping match count before incrementing it @@ -197,7 +197,7 @@ fn emit_preg_match_all_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_preg_match_all_done_linux_x86_64"); emitter.instruction("lea rdi, [rsp]"); // reload the compiled regex_t storage before freeing it with regfree() - emitter.bl_c("pcre2_regfree"); // release the compiled PCRE2 regex resources before returning the match count + emitter.emit_call_c("pcre2_regfree"); // release the compiled PCRE2 regex resources before returning the match count emitter.instruction(&format!("mov rax, QWORD PTR [rsp + {}]", match_count_off)); // return the total number of non-overlapping regex matches discovered in the subject emitter.instruction("jmp __rt_preg_match_all_ret_linux_x86_64"); // share the common epilogue after the successful match-count path diff --git a/src/codegen_support/runtime/system/preg_replace.rs b/src/codegen_support/runtime/system/preg_replace.rs index 060e3f2108..8e230685c7 100644 --- a/src/codegen_support/runtime/system/preg_replace.rs +++ b/src/codegen_support/runtime/system/preg_replace.rs @@ -333,7 +333,7 @@ fn emit_preg_replace_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("lea rdi, [rsp]"); // pass the local regex_t storage as the first regcomp() argument emitter.instruction(&format!("mov rsi, QWORD PTR [rsp + {}]", pattern_cstr_off)); // pass the null-terminated PCRE pattern C string as the second regcomp() argument emitter.instruction(&format!("mov edx, DWORD PTR [rsp + {}]", flags_off)); // pass PCRE2 POSIX compile flags from delimiter parsing - emitter.bl_c("pcre2_regcomp"); // compile the PCRE pattern into the local regex_t storage + emitter.emit_call_c("pcre2_regcomp"); // compile the PCRE pattern into the local regex_t storage emitter.instruction("test eax, eax"); // did regcomp() succeed and produce a compiled regex object? emitter.instruction("jnz __rt_preg_replace_fail_linux_x86_64"); // failed regex compilation maps to returning the original subject unchanged emitter.instruction(&format!("mov rax, QWORD PTR [rsp + {}]", subject_ptr_off)); // reload the elephc subject pointer before null-terminating it in the secondary scratch buffer @@ -360,7 +360,7 @@ fn emit_preg_replace_linux_x86_64(emitter: &mut Emitter) { emitter.instruction(&format!("mov edx, {}", PREG_REPLACE_NMATCH)); // capture full match plus replacement backreference groups emitter.instruction(&format!("lea rcx, [rsp + {}]", regmatch_off)); // pass the local regmatch_t buffer as the match extent output slot emitter.instruction("xor r8d, r8d"); // pass eflags = 0 so regexec() matches from the current subject cursor - emitter.bl_c("pcre2_regexec"); // execute the compiled PCRE2 regex at the current subject cursor + emitter.emit_call_c("pcre2_regexec"); // execute the compiled PCRE2 regex at the current subject cursor emitter.instruction("test eax, eax"); // did regexec() find another match at or after the current cursor? emitter.instruction("jnz __rt_preg_replace_tail_linux_x86_64"); // copy the remaining subject tail once regexec() reports no further matches emitter.instruction(&load_rm_so); // load rm_so from the native Linux regmatch_t layout using the correct regoff_t width @@ -475,7 +475,7 @@ fn emit_preg_replace_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_preg_replace_done_linux_x86_64"); emitter.instruction(&format!("mov QWORD PTR [rsp + {}], r11", output_write_off)); // preserve the final replacement output write cursor before finalizing the string result emitter.instruction("lea rdi, [rsp]"); // reload the compiled regex_t storage before freeing it with regfree() - emitter.bl_c("pcre2_regfree"); // release the compiled PCRE2 regex resources before returning the replacement result + emitter.emit_call_c("pcre2_regfree"); // release the compiled PCRE2 regex resources before returning the replacement result emitter.instruction(&format!("mov rax, QWORD PTR [rsp + {}]", output_start_off)); // reload the replacement output start pointer from the concat scratch buffer emitter.instruction(&format!("mov rdx, QWORD PTR [rsp + {}]", output_write_off)); // reload the final replacement output write cursor before computing the string length emitter.instruction("sub rdx, rax"); // compute the replacement output length from the output start and final write cursor diff --git a/src/codegen_support/runtime/system/preg_replace_callback.rs b/src/codegen_support/runtime/system/preg_replace_callback.rs index bb6759aa77..82e11a5fea 100644 --- a/src/codegen_support/runtime/system/preg_replace_callback.rs +++ b/src/codegen_support/runtime/system/preg_replace_callback.rs @@ -434,7 +434,7 @@ fn emit_preg_replace_callback_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("lea rdi, [rsp]"); // pass local regex_t storage to regcomp emitter.instruction(&format!("mov rsi, QWORD PTR [rsp + {}]", pattern_cstr_off)); // pass null-terminated PCRE pattern to regcomp emitter.instruction(&format!("mov edx, DWORD PTR [rsp + {}]", flags_off)); // pass PCRE2 POSIX compile flags from delimiter parsing - emitter.bl_c("pcre2_regcomp"); // compile regex through PCRE2 + emitter.emit_call_c("pcre2_regcomp"); // compile regex through PCRE2 emitter.instruction("test eax, eax"); // did regex compilation succeed? emitter.instruction("jnz __rt_preg_replace_callback_fail_linux_x86_64"); // return original subject when regex compilation fails @@ -447,7 +447,7 @@ fn emit_preg_replace_callback_linux_x86_64(emitter: &mut Emitter) { } else { emitter.instruction("shl rdi, 3"); // malloc bytes = nmatch * 8-byte regmatch_t slots } - emitter.bl_c("malloc"); // allocate the regmatch_t vector for all capture groups + emitter.emit_call_c("malloc"); // allocate the regmatch_t vector for all capture groups emitter.instruction("test rax, rax"); // did malloc return a capture buffer? emitter.instruction("jz __rt_preg_replace_callback_malloc_fail_linux_x86_64"); // allocation failure frees regex_t and returns the subject emitter.instruction(&format!("mov QWORD PTR [rsp + {}], rax", regmatches_ptr_off)); // save dynamic regmatch_t buffer pointer @@ -479,7 +479,7 @@ fn emit_preg_replace_callback_linux_x86_64(emitter: &mut Emitter) { emitter.instruction(&format!("mov rdx, QWORD PTR [rsp + {}]", nmatch_off)); // request one regmatch slot for every compiled capture group emitter.instruction(&format!("mov rcx, QWORD PTR [rsp + {}]", regmatches_ptr_off)); // pass dynamic regmatch_t capture buffer emitter.instruction("xor r8d, r8d"); // use default regexec execution flags - emitter.bl_c("pcre2_regexec"); // execute regex at the current subject cursor + emitter.emit_call_c("pcre2_regexec"); // execute regex at the current subject cursor emitter.instruction("test eax, eax"); // did regexec find another match? emitter.instruction("jnz __rt_preg_replace_callback_tail_linux_x86_64"); // copy the remaining subject once no more matches exist @@ -561,7 +561,7 @@ fn emit_preg_replace_callback_linux_x86_64(emitter: &mut Emitter) { emitter.instruction(&format!("mov rsi, QWORD PTR [rsp + {}]", callback_env_off)); // pass capture environment after visible callback args emitter.label("__rt_preg_replace_callback_direct_linux_x86_64"); emitter.instruction(&format!("mov r10, QWORD PTR [rsp + {}]", callback_ptr_off)); // reload callback entry point - emitter.instruction("call r10"); // call callback and receive replacement string in rax/rdx + emitter.emit_platform_callback_call("r10", 2); emitter.instruction("call __rt_str_persist"); // copy callback result away from volatile concat-buffer scratch space emitter.instruction(&format!("mov QWORD PTR [rsp + {}], rax", callback_result_ptr_off)); // save persisted callback result pointer across prefix copying emitter.instruction(&format!("mov QWORD PTR [rsp + {}], rdx", callback_result_len_off)); // save persisted callback result length across prefix copying @@ -640,9 +640,9 @@ fn emit_preg_replace_callback_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_preg_replace_callback_done_linux_x86_64"); emitter.instruction(&format!("mov QWORD PTR [rsp + {}], r11", output_write_off)); // save final output pointer emitter.instruction("lea rdi, [rsp]"); // pass regex_t storage to regfree - emitter.bl_c("pcre2_regfree"); // release compiled regex resources + emitter.emit_call_c("pcre2_regfree"); // release compiled regex resources emitter.instruction(&format!("mov rdi, QWORD PTR [rsp + {}]", regmatches_ptr_off)); // reload dynamic capture buffer for cleanup - emitter.bl_c("free"); // release the reusable regmatch_t vector + emitter.emit_call_c("free"); // release the reusable regmatch_t vector emitter.instruction(&format!("mov rax, QWORD PTR [rsp + {}]", output_start_off)); // return output start pointer emitter.instruction(&format!("mov rdx, QWORD PTR [rsp + {}]", output_write_off)); // reload output end pointer emitter.instruction("sub rdx, rax"); // compute output byte length @@ -657,7 +657,7 @@ fn emit_preg_replace_callback_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_preg_replace_callback_malloc_fail_linux_x86_64"); emitter.instruction("lea rdi, [rsp]"); // reload regex_t storage after capture-buffer allocation failed - emitter.bl_c("pcre2_regfree"); // free compiled regex resources before returning the subject + emitter.emit_call_c("pcre2_regfree"); // free compiled regex resources before returning the subject emitter.instruction(&format!("mov rax, QWORD PTR [rsp + {}]", subject_ptr_off)); // return original subject pointer after allocation failure emitter.instruction(&format!("mov rdx, QWORD PTR [rsp + {}]", subject_len_off)); // return original subject length after allocation failure @@ -720,3 +720,39 @@ fn emit_x86_load_regoff_from_ptr( emitter.instruction(&format!("movsxd {dst}, DWORD PTR [{addr}{suffix}]")); // sign-extend native 32-bit regoff_t from computed slot } } + +#[cfg(test)] +mod tests { + use crate::codegen_support::emit::Emitter; + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::*; + + /// Verifies the windows-x86_64 `__rt_preg_replace_callback` callback call + /// site emits the reverse-ABI SysV->MSx64 remap immediately before the + /// indirect `call r10` into the generated regex callback (finding F1, + /// reverse-ABI): without it, the generated callback would read the + /// matches-array/env arguments from the wrong registers on windows-x86_64. + #[test] + fn test_windows_x86_64_preg_replace_callback_remaps_before_indirect_call() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_preg_replace_callback(&mut emitter); + let asm = emitter.output(); + + let remap_idx = asm.find("mov rcx, rdi").expect("expected SysV->MSx64 remap"); + let call_idx = asm.find("call r11").expect("expected relocated indirect call r11"); + assert!(remap_idx < call_idx, "remap must precede the indirect callback call"); + } + + /// Verifies linux-x86_64 emission stays byte-identical to before the + /// reverse-ABI remap was introduced: the remap is windows-x86_64-only, so a + /// linux-x86_64 build must never see a `mov rcx, rdi` instruction. + #[test] + fn test_linux_x86_64_preg_replace_callback_has_no_reverse_abi_remap() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_preg_replace_callback(&mut emitter); + let asm = emitter.output(); + + assert!(!asm.contains("mov rcx, rdi")); + } +} diff --git a/src/codegen_support/runtime/system/preg_split.rs b/src/codegen_support/runtime/system/preg_split.rs index 377d6a55e6..d71f8ba90c 100644 --- a/src/codegen_support/runtime/system/preg_split.rs +++ b/src/codegen_support/runtime/system/preg_split.rs @@ -582,7 +582,7 @@ fn emit_preg_split_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("lea rdi, [rsp]"); // pass local regex_t storage to PCRE2 emitter.instruction(&format!("mov rsi, QWORD PTR [rsp + {}]", pattern_cstr_off)); // pass null-terminated PCRE pattern to PCRE2 emitter.instruction(&format!("mov edx, DWORD PTR [rsp + {}]", regex_flags_off)); // pass PCRE2 POSIX compile flags from delimiter parsing - emitter.bl_c("pcre2_regcomp"); // compile regex through PCRE2 + emitter.emit_call_c("pcre2_regcomp"); // compile regex through PCRE2 emitter.instruction("test eax, eax"); // did regex compilation succeed? emitter.instruction("jnz __rt_preg_split_fail_linux_x86_64"); // return an empty result array on compilation failure @@ -595,7 +595,7 @@ fn emit_preg_split_linux_x86_64(emitter: &mut Emitter) { } else { emitter.instruction("shl rdi, 3"); // malloc bytes = nmatch * 8-byte regmatch_t slots } - emitter.bl_c("malloc"); // allocate the regmatch_t vector for all capture groups + emitter.emit_call_c("malloc"); // allocate the regmatch_t vector for all capture groups emitter.instruction("test rax, rax"); // did malloc return a capture buffer? emitter.instruction("jz __rt_preg_split_malloc_fail_linux_x86_64"); // allocation failure frees regex_t and returns an empty array emitter.instruction(&format!("mov QWORD PTR [rsp + {}], rax", regmatches_ptr_off)); // save dynamic regmatch_t buffer pointer @@ -629,7 +629,7 @@ fn emit_preg_split_linux_x86_64(emitter: &mut Emitter) { emitter.instruction(&format!("mov rdx, QWORD PTR [rsp + {}]", nmatch_off)); // request one regmatch slot for every compiled capture group emitter.instruction(&format!("mov rcx, QWORD PTR [rsp + {}]", regmatches_ptr_off)); // pass dynamic regmatch_t capture buffer emitter.instruction("xor r8d, r8d"); // eflags = 0 for ordinary matching - emitter.bl_c("pcre2_regexec"); // execute regex against remaining subject + emitter.emit_call_c("pcre2_regexec"); // execute regex against remaining subject emitter.instruction("test eax, eax"); // did regexec find another separator? emitter.instruction("jnz __rt_preg_split_last_linux_x86_64"); // no more matches means the trailing segment remains @@ -713,9 +713,9 @@ fn emit_preg_split_linux_x86_64(emitter: &mut Emitter) { mixed_ptr_off, ); emitter.instruction("lea rdi, [rsp]"); // reload compiled regex_t storage before freeing - emitter.bl_c("pcre2_regfree"); // release PCRE2 regex resources + emitter.emit_call_c("pcre2_regfree"); // release PCRE2 regex resources emitter.instruction(&format!("mov rdi, QWORD PTR [rsp + {}]", regmatches_ptr_off)); // reload dynamic capture buffer for cleanup - emitter.bl_c("free"); // release the reusable regmatch_t vector + emitter.emit_call_c("free"); // release the reusable regmatch_t vector emitter.instruction(&format!("mov rax, QWORD PTR [rsp + {}]", array_ptr_off)); // return final result array pointer emitter.instruction("jmp __rt_preg_split_ret_linux_x86_64"); // share common epilogue @@ -726,7 +726,7 @@ fn emit_preg_split_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_preg_split_malloc_fail_linux_x86_64"); emitter.instruction("lea rdi, [rsp]"); // reload compiled regex_t storage after allocation failure - emitter.bl_c("pcre2_regfree"); // release PCRE2 regex resources before returning empty + emitter.emit_call_c("pcre2_regfree"); // release PCRE2 regex resources before returning empty emit_preg_split_alloc_result_x86_64(emitter, "malloc_fail", preg_flags_off, array_ptr_off); emitter.instruction(&format!("mov rax, QWORD PTR [rsp + {}]", array_ptr_off)); // return empty result array pointer after allocation failure diff --git a/src/codegen_support/runtime/system/regex_locale.rs b/src/codegen_support/runtime/system/regex_locale.rs index 464ff3936a..f905b5ed9e 100644 --- a/src/codegen_support/runtime/system/regex_locale.rs +++ b/src/codegen_support/runtime/system/regex_locale.rs @@ -47,12 +47,12 @@ pub(crate) fn emit_prepare_regex_locale(emitter: &mut Emitter) { Arch::X86_64 => { emitter.instruction(&format!("mov edi, {}", emitter.platform.lc_ctype())); // select LC_CTYPE so character classes use the environment locale abi::emit_symbol_address(emitter, "rsi", "_locale_utf8_name"); - emitter.bl_c("setlocale"); // activate the UTF-8 locale category before compiling regex + emitter.emit_call_c("setlocale"); // activate the UTF-8 locale category before compiling regex emitter.instruction("test rax, rax"); // check whether the explicit UTF-8 locale was accepted emitter.instruction("jnz 1f"); // skip fallback when the explicit UTF-8 locale is available emitter.instruction(&format!("mov edi, {}", emitter.platform.lc_ctype())); // reselect LC_CTYPE for the environment-locale fallback abi::emit_symbol_address(emitter, "rsi", "_locale_env_name"); - emitter.bl_c("setlocale"); // try the environment locale when C.UTF-8 is unavailable + emitter.emit_call_c("setlocale"); // try the environment locale when C.UTF-8 is unavailable emitter.label("1"); } } diff --git a/src/codegen_support/runtime/system/serialize.rs b/src/codegen_support/runtime/system/serialize.rs index a2ce5a78c6..32274fd4e1 100644 --- a/src/codegen_support/runtime/system/serialize.rs +++ b/src/codegen_support/runtime/system/serialize.rs @@ -1435,7 +1435,7 @@ fn emit_serialize_x86_64(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [rbp - 32], r10"); // save it so method scratch can be rewound away emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // $this receiver for the method call emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the __serialize target - emitter.instruction("call r10"); // __serialize($this) -> rax = array (bare pointer) + emitter.emit_platform_callback_call("r10", 1); emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the returned array pointer emit_symbol_address(emitter, "r10", "_concat_off"); emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the saved post-prefix offset @@ -1468,7 +1468,7 @@ fn emit_serialize_x86_64(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [rbp - 32], r10"); // save it for the scratch rewind emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // $this receiver emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the __sleep target - emitter.instruction("call r10"); // __sleep($this) -> rax = names array (indexed) + emitter.emit_platform_callback_call("r10", 1); emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the names array pointer emit_symbol_address(emitter, "r10", "_concat_off"); emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the saved post-prefix offset diff --git a/src/codegen_support/runtime/system/shell_exec.rs b/src/codegen_support/runtime/system/shell_exec.rs index 7bc94afcaa..68982475db 100644 --- a/src/codegen_support/runtime/system/shell_exec.rs +++ b/src/codegen_support/runtime/system/shell_exec.rs @@ -61,7 +61,7 @@ pub fn emit_shell_exec(emitter: &mut Emitter) { // -- read loop: fgetc until EOF -- emitter.label("__rt_shell_exec_loop"); emitter.instruction("ldr x0, [sp, #0]"); // reload FILE* from stack - emitter.bl_c("fgetc"); // read one byte → w0=char or -1 (EOF) + emitter.emit_call_c("fgetc"); // read one byte → w0=char or -1 (EOF) emitter.instruction("cmn w0, #1"); // compare 32-bit return with -1 (EOF) emitter.instruction("b.eq __rt_shell_exec_close"); // if EOF, stop reading @@ -118,7 +118,7 @@ fn emit_shell_exec_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_shell_exec_loop"); emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the FILE* for the next fgetc() probe - emitter.bl_c("fgetc"); // read one byte from the pipe → eax=byte or -1 (EOF) + emitter.emit_call_c("fgetc"); // read one byte from the pipe → eax=byte or -1 (EOF) emitter.instruction("cmp eax, -1"); // did the pipe reach EOF? emitter.instruction("je __rt_shell_exec_close"); // stop reading once the full shell output has been consumed diff --git a/src/codegen_support/runtime/system/strtotime/keywords.rs b/src/codegen_support/runtime/system/strtotime/keywords.rs index 2b93db504b..9c0d6c544a 100644 --- a/src/codegen_support/runtime/system/strtotime/keywords.rs +++ b/src/codegen_support/runtime/system/strtotime/keywords.rs @@ -257,7 +257,7 @@ fn emit_today_tm_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rax, QWORD PTR [rip + _strtotime_clock]"); // rax = effective clock (base timestamp or current time) emitter.instruction("mov QWORD PTR [rsp], rax"); // save ts at top of sub-frame emitter.instruction("mov rdi, rsp"); // rdi = &ts for libc localtime - emitter.instruction("call localtime"); // rax = static struct tm* + emitter.emit_call_c("localtime"); // rax = static struct tm* // -- copy 36 bytes from libc tm into dispatcher tm scratch (caller's struct tm at [rbp - 128..rbp - 92]) -- emitter.instruction("mov rcx, QWORD PTR [rax + 0]"); // load tm_sec/tm_min/tm_hour/tm_mday (8 bytes) emitter.instruction("mov QWORD PTR [rbp - 128], rcx"); // store first 8 bytes of tm @@ -292,7 +292,7 @@ fn emit_now_tm_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rax, QWORD PTR [rip + _strtotime_clock]"); // rax = effective clock (base timestamp or current time) emitter.instruction("mov QWORD PTR [rsp], rax"); // save ts at top of sub-frame emitter.instruction("mov rdi, rsp"); // rdi = &ts for libc localtime - emitter.instruction("call localtime"); // rax = static struct tm* + emitter.emit_call_c("localtime"); // rax = static struct tm* emitter.instruction("mov rcx, QWORD PTR [rax + 0]"); // load tm_sec/tm_min/tm_hour/tm_mday emitter.instruction("mov QWORD PTR [rbp - 128], rcx"); // store first 8 bytes of tm emitter.instruction("mov rcx, QWORD PTR [rax + 8]"); // load 8 more bytes diff --git a/src/codegen_support/runtime/system/time.rs b/src/codegen_support/runtime/system/time.rs index 21c8a8cbc5..c87b748cfe 100644 --- a/src/codegen_support/runtime/system/time.rs +++ b/src/codegen_support/runtime/system/time.rs @@ -91,7 +91,7 @@ fn emit_time_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("sub rsp, 32"); // reserve aligned stack storage for one timeval struct plus scratch padding before the libc call emitter.instruction("lea rdi, [rsp]"); // pass the temporary timeval storage as the first SysV integer argument to libc gettimeofday() emitter.instruction("xor esi, esi"); // pass NULL as the timezone pointer because elephc only needs the current Unix timestamp - emitter.bl_c("gettimeofday"); // fill the temporary timeval with the current wall-clock time through libc + emitter.emit_call_c("gettimeofday"); // fill the temporary timeval with the current wall-clock time through libc emitter.instruction("mov rax, QWORD PTR [rsp]"); // return tv_sec from the temporary timeval as the current Unix timestamp in the native integer result register emitter.instruction("leave"); // release the temporary timeval storage and restore the caller frame pointer in one step emitter.instruction("ret"); // return the current Unix timestamp to generated code diff --git a/src/codegen_support/runtime/system/unserialize.rs b/src/codegen_support/runtime/system/unserialize.rs index ef937f509c..6d97d40403 100644 --- a/src/codegen_support/runtime/system/unserialize.rs +++ b/src/codegen_support/runtime/system/unserialize.rs @@ -697,6 +697,43 @@ fn emit_unser_key_aarch64(emitter: &mut Emitter) { emitter.instruction("ret"); // return the integer key } +#[cfg(test)] +mod tests { + //! Purpose: + //! Structural regressions for the target-specific serialized-value parser assembly. + //! + //! Called from: + //! - `cargo test --lib` through Rust's test harness. + //! + //! Key details: + //! - Windows must parse PHP's uppercase non-finite wire tokens before its CRT `strtod` fallback. + + use crate::codegen_support::emit::Emitter; + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::emit_unserialize; + + /// Verifies Windows recognizes PHP's INF, -INF, and NAN wire tokens without relying on msvcrt `strtod`. + #[test] + fn windows_unserialize_non_finite_tokens_bypass_crt_strtod() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_unserialize(&mut emitter); + let asm = emitter.output(); + + for bits in [ + "mov r9, 0x7ff0000000000000", + "mov r9, 0xfff0000000000000", + "mov r9, 0x7ff8000000000000", + ] { + assert!(asm.contains(bits), "missing non-finite bit pattern: {bits}"); + } + assert!( + asm.contains("call __rt_sys_strtod"), + "ordinary serialized float values must retain the Windows ABI-safe strtod fallback" + ); + } +} + /// x86_64 implementation of the unserialize entry, recursive parser, and key parser. fn emit_unserialize_x86_64(emitter: &mut Emitter) { // -- entry wrapper: __rt_unser_at(base=ptr, pos=0, end=len) -- @@ -818,9 +855,56 @@ fn emit_unserialize_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload base emitter.instruction("add rdi, QWORD PTR [rbp - 16]"); // pointer to the type byte emitter.instruction("add rdi, 2"); // strtod source = first byte after "d:" + // -- PHP's serialized non-finite spellings are not accepted by every CRT -- + // MinGW's Windows `strtod` rejects the upper-case PHP wire tokens, whereas + // its Unix counterparts accept them. Recognize their exact, semicolon-ended + // forms before falling back to libc for ordinary decimal/exponent values. + emitter.instruction("mov r10, rdi"); // keep the wire-token cursor while matching its bytes + emitter.instruction("cmp BYTE PTR [r10], 73"); // starts with ASCII 'I' for positive infinity? + emitter.instruction("jne __rt_unser_at_float_neg_inf"); // try the negative-infinity spelling next + emitter.instruction("cmp BYTE PTR [r10 + 1], 78"); // second byte is ASCII 'N'? + emitter.instruction("jne __rt_unser_at_float_nan"); // not INF: try NAN before libc + emitter.instruction("cmp BYTE PTR [r10 + 2], 70"); // third byte is ASCII 'F'? + emitter.instruction("jne __rt_unser_at_float_nan"); // malformed INF falls back through NAN/libc checks + emitter.instruction("cmp BYTE PTR [r10 + 3], 59"); // token must end at the PHP ';' delimiter + emitter.instruction("jne __rt_unser_at_float_libc"); // longer/malformed text is handled by libc + emitter.instruction("mov r9, 0x7ff0000000000000"); // IEEE-754 positive infinity bit pattern + emitter.instruction("add r10, 3"); // endptr-equivalent cursor points at the delimiter + emitter.instruction("mov QWORD PTR [rbp - 72], r10"); // retain the cursor across Mixed boxing + emitter.instruction("jmp __rt_unser_at_float_box"); // box the recognized positive infinity + emitter.label("__rt_unser_at_float_neg_inf"); + emitter.instruction("cmp BYTE PTR [r10], 45"); // starts with ASCII '-' for negative infinity? + emitter.instruction("jne __rt_unser_at_float_nan"); // not negative infinity: try NAN + emitter.instruction("cmp BYTE PTR [r10 + 1], 73"); // second byte is ASCII 'I'? + emitter.instruction("jne __rt_unser_at_float_nan"); // malformed -INF: try NAN + emitter.instruction("cmp BYTE PTR [r10 + 2], 78"); // third byte is ASCII 'N'? + emitter.instruction("jne __rt_unser_at_float_nan"); // malformed -INF: try NAN + emitter.instruction("cmp BYTE PTR [r10 + 3], 70"); // fourth byte is ASCII 'F'? + emitter.instruction("jne __rt_unser_at_float_nan"); // malformed -INF: try NAN + emitter.instruction("cmp BYTE PTR [r10 + 4], 59"); // token must end at the PHP ';' delimiter + emitter.instruction("jne __rt_unser_at_float_libc"); // longer/malformed text is handled by libc + emitter.instruction("mov r9, 0xfff0000000000000"); // IEEE-754 negative infinity bit pattern + emitter.instruction("add r10, 4"); // endptr-equivalent cursor points at the delimiter + emitter.instruction("mov QWORD PTR [rbp - 72], r10"); // retain the cursor across Mixed boxing + emitter.instruction("jmp __rt_unser_at_float_box"); // box the recognized negative infinity + emitter.label("__rt_unser_at_float_nan"); + emitter.instruction("cmp BYTE PTR [r10], 78"); // starts with ASCII 'N' for NaN? + emitter.instruction("jne __rt_unser_at_float_libc"); // ordinary numeric text uses libc + emitter.instruction("cmp BYTE PTR [r10 + 1], 65"); // second byte is ASCII 'A'? + emitter.instruction("jne __rt_unser_at_float_libc"); // malformed NAN uses libc + emitter.instruction("cmp BYTE PTR [r10 + 2], 78"); // third byte is ASCII 'N'? + emitter.instruction("jne __rt_unser_at_float_libc"); // malformed NAN uses libc + emitter.instruction("cmp BYTE PTR [r10 + 3], 59"); // token must end at the PHP ';' delimiter + emitter.instruction("jne __rt_unser_at_float_libc"); // longer/malformed text is handled by libc + emitter.instruction("mov r9, 0x7ff8000000000000"); // canonical quiet-NaN bit pattern + emitter.instruction("add r10, 3"); // endptr-equivalent cursor points at the delimiter + emitter.instruction("mov QWORD PTR [rbp - 72], r10"); // retain the cursor across Mixed boxing + emitter.instruction("jmp __rt_unser_at_float_box"); // box the recognized NaN + emitter.label("__rt_unser_at_float_libc"); emitter.instruction("lea rsi, [rbp - 72]"); // strtod endptr = &scratch - emitter.instruction("call strtod"); // parse the float (stops at ';') -> xmm0, scratch=endptr + emitter.emit_call_c("strtod"); // parse the float (stops at ';') -> xmm0, scratch=endptr emitter.instruction("movq r9, xmm0"); // move the parsed double into a GPR + emitter.label("__rt_unser_at_float_box"); emitter.instruction("mov rdi, r9"); // value payload = float bits emitter.instruction("mov rax, 2"); // value tag = float emitter.instruction("mov rsi, 0"); // float high payload unused @@ -1024,7 +1108,7 @@ fn emit_unserialize_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // $this receiver = first argument emitter.instruction("mov rsi, QWORD PTR [rbp - 80]"); // $data assoc array (bare hash) = second argument emitter.instruction("mov r10, QWORD PTR [rbp - 72]"); // reload the __unserialize target - emitter.instruction("call r10"); // call __unserialize($this, $data) + emitter.emit_platform_callback_call("r10", 2); emitter.instruction("jmp __rt_unser_at_obj_box"); // box the object (position is at the closing '}') emitter.label("__rt_unser_obj_default"); emitter.instruction("mov QWORD PTR [rbp - 48], 0"); // initialize the property index @@ -1062,7 +1146,7 @@ fn emit_unserialize_x86_64(emitter: &mut Emitter) { emitter.instruction("test r10, r10"); // does the class define __wakeup? emitter.instruction("jz __rt_unser_at_obj_box"); // no → box the object directly emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // $this receiver - emitter.instruction("call r10"); // call __wakeup($this) + emitter.emit_platform_callback_call("r10", 1); emitter.label("__rt_unser_at_obj_box"); emitter.instruction("mov rax, 24"); // box the object: Mixed cell = tag + two payload words emitter.instruction("call __rt_heap_alloc"); // allocate the boxed Mixed cell diff --git a/src/codegen_support/runtime/win32/imports.rs b/src/codegen_support/runtime/win32/imports.rs new file mode 100644 index 0000000000..325308d713 --- /dev/null +++ b/src/codegen_support/runtime/win32/imports.rs @@ -0,0 +1,383 @@ +//! Purpose: +//! Declares Win32 API imports and maps imported C symbols to runtime ABI shims. +//! +//! Called from: +//! - `crate::codegen_support::runtime::win32::emit_win32_shims()`. +//! +//! Key details: +//! - Every Win32 API referenced by emitted assembly is declared explicitly. +//! - C-library calls that need SysV-to-MSx64 adaptation resolve through dedicated shim labels. + +use crate::codegen::emit::Emitter; + +/// Emits `.extern` declarations for all Win32 API functions used by the shims. +pub(super) fn emit_win32_imports(emitter: &mut Emitter) { + emitter.raw(" # -- Win32 API imports (resolved by MinGW linker against kernel32/msvcrt) --"); + for func in WIN32_IMPORTS { + emitter.raw(&format!(".extern {}", func)); + } + emitter.blank(); +} + +/// Win32 API functions imported by the shims. +const WIN32_IMPORTS: &[&str] = &[ + "GetStdHandle", + "WriteFile", + "ReadFile", + "GetLastError", + "FormatMessageW", + "MultiByteToWideChar", + "WideCharToMultiByte", + "CompareStringOrdinal", + "CloseHandle", + "ExitProcess", + "GetCommandLineW", + "CommandLineToArgvW", + "LocalFree", + "GetProcessHeap", + "HeapAlloc", + "HeapFree", + "VirtualAlloc", + "VirtualFree", + "VirtualProtect", + "GetCurrentProcessId", + "GetProcessId", + "GetSystemTimeAsFileTime", + "QueryPerformanceCounter", + "QueryPerformanceFrequency", + "BCryptGenRandom", + "CreateFileW", + "SetFilePointer", + "GetFileType", + "GetConsoleMode", + "DeleteFileW", + "GetCurrentDirectoryW", + "SetCurrentDirectoryW", + "CreateDirectoryW", + "RemoveDirectoryW", + "GetFileAttributesW", + "GetFileAttributesExW", + "GetFileSizeEx", + "MoveFileExW", + "SetFileAttributesW", + "GetDiskFreeSpaceExW", + "GetTempPathW", + "GetTempFileNameW", + "GetComputerNameW", + "GetNativeSystemInfo", + "GetFileInformationByHandle", + "DeviceIoControl", + "gethostname", + "gethostbyname", + "getprotobyname", + "getprotobynumber", + "getservbyname", + "getservbyport", + "socket", + "WSASocketW", + "connect", + "bind", + "listen", + "accept", + "send", + "recv", + "sendto", + "recvfrom", + "WSASend", + "WSARecv", + "shutdown", + "closesocket", + "getsockname", + "getpeername", + "setsockopt", + "getsockopt", + "ioctlsocket", + "select", + "WSAGetLastError", + "getpid", + "_putenv", + "uname", + "sysinfo", + "execve", + "kill", + "futex", + "FlushFileBuffers", + "LockFileEx", + "UnlockFileEx", + "CreateSymbolicLinkW", + "CreateHardLinkW", + "FindFirstFileExW", + "FindNextFileW", + "FindClose", + "GetFullPathNameW", + "GetFinalPathNameByHandleW", + "GetBinaryTypeW", + "SetFileTime", + "_mkgmtime", + "_execvp", + "_popen", + "_pclose", + "_fileno", + "fgetc", + "system", + "strtod", + "snprintf", + "strtol", + "OpenProcess", + "TerminateProcess", + "GlobalMemoryStatusEx", + "PathMatchSpecW", + "_dup", + "_dup2", + "_open_osfhandle", + "_get_osfhandle", + "_close", + "WSAStartup", + "WSACleanup", + "SetFilePointerEx", + "SetEndOfFile", + "Sleep", + "GetProcessTimes", + "getenv", + "_tzset", + "time", + "localtime", + "gmtime", + "mktime", + "pow", + "sin", + "cos", + "tan", + "asin", + "acos", + "atan", + "sinh", + "cosh", + "tanh", + "exp", + "log", + "log2", + "log10", + "atan2", + "hypot", + "fmod", + "round", + "compressBound", + "deflateEnd", + "inflateEnd", + "deflate", + "inflate", + "uncompress", + "inflateInit2_", + "compress2", + "deflateInit2_", + // W3g bzip2 family: statically linked from the MinGW-sysroot `libbz2.a` + // (via `ELEPHC_MINGW_SYSROOT`, `src/linker.rs:406`, `-lbz2`), MSx64 ABI — + // same pattern as the W3d zlib family above. See `emit_shim_bzip2`. + "BZ2_bzCompress", + "BZ2_bzCompressInit", + "BZ2_bzCompressEnd", + "BZ2_bzBuffToBuffDecompress", + "pcre2_regcomp", + "pcre2_regexec", + "pcre2_regfree", + "malloc", + "free", + // W3e-2 net/dns/inet (ws2_32) + misc (msvcrt) family. + "getaddrinfo", + "freeaddrinfo", + "inet_pton", + "inet_ntop", + "gethostbyaddr", + "_strtoi64", + "atof", + "setlocale", + // W3f-A iconv family: statically linked from the MinGW-sysroot `libiconv.a` + // (via `ELEPHC_MINGW_SYSROOT`, `src/linker.rs:256/406`, `-liconv`), MSx64 + // ABI — same pattern as the W3d zlib / W3e-1 PCRE2-POSIX families above. + "iconv_open", + "iconv", + "iconv_close", + // W3f-B rewrites: standard msvcrt symbols (real ABI shims — see + // `windows_c_shim_name` doc for the per-symbol msvcrt-existence verdict). + "fopen", + "fgets", + "fclose", + "strncmp", + "strchr", + "strtoul", + // W6/C1c proc_open/proc_close family: real process spawning via + // `CreatePipe`/`CreateProcessW`/`WaitForSingleObject`/`GetExitCodeProcess` + // (`emit_proc_open_win32_x86_64` in `runtime/io/proc_open.rs`, and the + // Windows arm of `emit_proc_close` in `runtime/io/proc_close.rs`). + "CreatePipe", + "DuplicateHandle", + "PeekNamedPipe", + "CreateProcessW", + "WaitForSingleObject", + "GetExitCodeProcess", + "SetHandleInformation", + "SetErrorMode", + // Windows ACL family (advapi32, except GetCurrentThread which is + // kernel32): the security-descriptor + AccessCheck path php-src's + // TSRM/tsrm_win32.c uses for access(R_OK)/access(W_OK). Consumed by + // `emit_win_acl_helpers` in shims_fs.rs; advapi32 is added to the Windows + // link set in src/linker.rs and tests/codegen/support/runner.rs. + "GetCurrentThread", + "OpenThreadToken", + "ImpersonateSelf", + "RevertToSelf", + "GetFileSecurityW", + "MapGenericMask", + "AccessCheck", +]; + +/// C-library symbols that have a dedicated `__rt_sys_` Windows shim +/// emitted below (`emit_shim_strtod`, `emit_shim_strtol`, `emit_shim_snprintf`, +/// `emit_shim_gethostbyname`, the W3b datetime family — `emit_shim_getenv`, +/// `emit_shim_putenv`, `emit_shim_tzset`, `emit_shim_time`, `emit_shim_localtime`, +/// `emit_shim_gmtime`, `emit_shim_mktime`, `emit_shim_gettimeofday` — and the +/// W3c math/FP family emitted uniformly by [`emit_shim_math_fp`] / +/// [`emit_fp_shadow_shim`] over [`MATH_FP_SHIM_SYMBOLS`], and the W3d zlib +/// family — `compressBound`, `deflateEnd`, `inflateEnd`, `deflate`, +/// `inflate`, `uncompress`, `inflateInit2_`, `compress2`, `deflateInit2_`, +/// emitted by [`emit_shim_zlib`] — statically linked from the MinGW-sysroot +/// `libz.a`, which is MSx64-ABI (built by MinGW gcc), NOT SysV, and the W3e-1 +/// PCRE2-POSIX/alloc family — `pcre2_regcomp`, `pcre2_regexec`, +/// `pcre2_regfree` (statically linked from the MinGW-sysroot +/// `libpcre2-posix.a`/`libpcre2-8.a`, also MSx64-ABI) and `malloc`/`free` +/// (standard msvcrt symbols), emitted by [`emit_shim_pcre2_posix`], +/// [`emit_shim_malloc`], and [`emit_shim_free`] — each backed by a Win32 API +/// import declared in +/// [`WIN32_IMPORTS`]. This is the SINGLE SOURCE OF TRUTH consulted by +/// `Emitter::emit_call_c` (`codegen_support::emit`): registering a new symbol +/// here, adding its `emit_shim_*` wrapper, adding its Win32 import name to +/// `WIN32_IMPORTS`, and calling the new `emit_shim_*` from `emit_win32_shims` +/// is the complete, one-place change needed to route a new msvcrt/ws2_32 call +/// correctly on windows-x86_64. Returns `None` for a symbol with no shim — +/// callers fall back to the SysV stub-delegate list or panic (see +/// `Emitter::emit_call_c`). +pub(crate) fn windows_c_shim_name(symbol: &str) -> Option<&'static str> { + match symbol { + "strtod" => Some("__rt_sys_strtod"), + "strtol" => Some("__rt_sys_strtol"), + "snprintf" => Some("__rt_sys_snprintf"), + "gethostbyname" => Some("__rt_sys_gethostbyname"), + "getenv" => Some("__rt_sys_getenv"), + "putenv" => Some("__rt_sys_putenv"), + "tzset" => Some("__rt_sys_tzset"), + "time" => Some("__rt_sys_time"), + "localtime" => Some("__rt_sys_localtime"), + "gmtime" => Some("__rt_sys_gmtime"), + "mktime" => Some("__rt_sys_mktime"), + "gettimeofday" => Some("__rt_sys_gettimeofday"), + "pow" => Some("__rt_sys_pow"), + "sin" => Some("__rt_sys_sin"), + "cos" => Some("__rt_sys_cos"), + "tan" => Some("__rt_sys_tan"), + "asin" => Some("__rt_sys_asin"), + "acos" => Some("__rt_sys_acos"), + "atan" => Some("__rt_sys_atan"), + "sinh" => Some("__rt_sys_sinh"), + "cosh" => Some("__rt_sys_cosh"), + "tanh" => Some("__rt_sys_tanh"), + "exp" => Some("__rt_sys_exp"), + "log" => Some("__rt_sys_log"), + "log2" => Some("__rt_sys_log2"), + "log10" => Some("__rt_sys_log10"), + "atan2" => Some("__rt_sys_atan2"), + "hypot" => Some("__rt_sys_hypot"), + "fmod" => Some("__rt_sys_fmod"), + "round" => Some("__rt_sys_round"), + "compressBound" => Some("__rt_sys_compressBound"), + "deflateEnd" => Some("__rt_sys_deflateEnd"), + "inflateEnd" => Some("__rt_sys_inflateEnd"), + "deflate" => Some("__rt_sys_deflate"), + "inflate" => Some("__rt_sys_inflate"), + "uncompress" => Some("__rt_sys_uncompress"), + "inflateInit2_" => Some("__rt_sys_inflateInit2_"), + "compress2" => Some("__rt_sys_compress2"), + "deflateInit2_" => Some("__rt_sys_deflateInit2_"), + // W3g bzip2 family — real ABI shims (libbz2 statically linked on + // Windows, same sysroot mechanism as the zlib family above). See + // `emit_shim_bzip2`. + "BZ2_bzCompress" => Some("__rt_sys_BZ2_bzCompress"), + "BZ2_bzCompressInit" => Some("__rt_sys_BZ2_bzCompressInit"), + "BZ2_bzCompressEnd" => Some("__rt_sys_BZ2_bzCompressEnd"), + "BZ2_bzBuffToBuffDecompress" => Some("__rt_sys_BZ2_bzBuffToBuffDecompress"), + "pcre2_regcomp" => Some("__rt_sys_pcre2_regcomp"), + "pcre2_regexec" => Some("__rt_sys_pcre2_regexec"), + "pcre2_regfree" => Some("__rt_sys_pcre2_regfree"), + "malloc" => Some("__rt_sys_malloc"), + "free" => Some("__rt_sys_free"), + // W3e-2 net/dns/inet (ws2_32) family — see `emit_shim_net_dns`. + "getaddrinfo" => Some("__rt_sys_getaddrinfo"), + "freeaddrinfo" => Some("__rt_sys_freeaddrinfo"), + "inet_pton" => Some("__rt_sys_inet_pton"), + "inet_ntop" => Some("__rt_sys_inet_ntop"), + "gethostbyaddr" => Some("__rt_sys_gethostbyaddr"), + // W3e-2 misc msvcrt family — see `emit_shim_net_dns`. + "strtoll" => Some("__rt_sys_strtoll"), + "atof" => Some("__rt_sys_atof"), + // `dup` already has an `__rt_sys_dup` shim (`emit_shim_dup_shims`, + // W3c) that calls msvcrt `_dup` with the required `cdqe` + // sign-extension — reused here rather than duplicated. + "dup" => Some("__rt_sys_dup"), + "setlocale" => Some("__rt_sys_setlocale"), + // `chown`/`lchown` are DELIBERATELY NOT routed to the pre-existing + // `__rt_sys_chown`/`__rt_sys_lchown` labels (`emit_shim_c_symbol_delegates`, + // used by the Linux-syscall-number 92/94 transform path — see + // `windows_transform.rs`), which return -1 (ENOSYS). php-src makes + // PHP-level `chown`/`lchown` fail silently on Windows; this distinct + // contract from the Linux syscall surface means that this + // W3e-2 libc-call-site family gets its own `__rt_sys_libc_chown`/ + // `__rt_sys_libc_lchown` shims instead of overloading the existing + // (unrelated call path's) labels. See `emit_shim_net_dns`. + "chown" => Some("__rt_sys_libc_chown"), + "lchown" => Some("__rt_sys_libc_lchown"), + // `dup2` already has an `__rt_sys_dup2` shim (`emit_shim_dup_shims`, + // W3c) that calls msvcrt `_dup2` with the required `cdqe` + // sign-extension — reused rather than duplicated. Consumers: + // `stream_filters/iconv.rs` (W3f-A) plus `stream_filters/inflate.rs` + // and `stream_filters/compress_bzip2_stream.rs` (W3g). + "dup2" => Some("__rt_sys_dup2"), + // W3f-A iconv family — real ABI shims (libiconv statically linked on + // Windows, see the `WIN32_IMPORTS` comment above). See + // `emit_shim_iconv`. + "iconv_open" => Some("__rt_sys_iconv_open"), + "iconv" => Some("__rt_sys_iconv"), + "iconv_close" => Some("__rt_sys_iconv_close"), + // W3f-B rewrites — msvcrt-real shims: fopen/fgets/fclose/strncmp/ + // strchr/strtoul all EXIST on msvcrt, so `principal_lookup.rs`'s + // passwd/group lookup gets real ABI shims rather than a bespoke + // stub; the "no /etc/passwd on Windows" behavior emerges naturally + // (fopen("/etc/passwd") -> NULL -> the lookup's existing fail path). + // See `emit_shim_msvcrt_passwd_lookup`. + "fopen" => Some("__rt_sys_fopen"), + "fgets" => Some("__rt_sys_fgets"), + "fclose" => Some("__rt_sys_fclose"), + "fgetc" => Some("__rt_sys_fgetc"), + "system" => Some("__rt_sys_system"), + "strncmp" => Some("__rt_sys_strncmp"), + "strchr" => Some("__rt_sys_strchr"), + "strtoul" => Some("__rt_sys_strtoul"), + // Directory and temporary-file operations have no direct msvcrt + // equivalents. These real ABI shims implement them with the Unicode + // Win32 APIs (`FindFirstFileExW`/`FindNextFileW`/`FindClose` and + // exclusive `CreateFileW` creation) while preserving the POSIX + // contracts expected by the shared runtime emitters. + "opendir" => Some("__rt_sys_opendir"), + "readdir" => Some("__rt_sys_readdir"), + "closedir" => Some("__rt_sys_closedir"), + "rewinddir" => Some("__rt_sys_rewinddir"), + "mkstemp" => Some("__rt_sys_mkstemp"), + // W3g: msvcrt has no `fdatasync` export. `fsync` (the bare + // stub-delegate label emitted by `emit_shim_c_symbol_delegates`, + // FlushFileBuffers-backed) already satisfies fdatasync's contract + // (flush data AND metadata), so `__rt_sys_fdatasync` tail-calls it + // rather than duplicating the body — same fallback the non-Windows + // Darwin path already takes (`modify_x86_64.rs`). + "fdatasync" => Some("__rt_sys_fdatasync"), + _ => None, + } +} diff --git a/src/codegen_support/runtime/win32/mod.rs b/src/codegen_support/runtime/win32/mod.rs new file mode 100644 index 0000000000..55accb6bfb --- /dev/null +++ b/src/codegen_support/runtime/win32/mod.rs @@ -0,0 +1,225 @@ +//! Purpose: +//! Emits Win32 API shim wrappers that convert SysV calling convention arguments +//! to MSx64 ABI and call the corresponding Win32 API functions. These shims are +//! the bridge between the existing Linux x86_64 runtime (which sets up arguments +//! in rdi/rsi/rdx/r10/rcx/r8/r9) and Windows kernel32/msvcrt functions. +//! +//! Called from: +//! - `crate::codegen::runtime::emitters::emit_runtime()` when target is Windows x86_64. +//! +//! Key details: +//! - Each shim takes arguments in SysV registers and shuffles them to MSx64 (rcx/rdx/r8/r9). +//! - 32-byte shadow space is allocated before each Win32 call and freed after. +//! - Stack is aligned to 16 bytes before each call. +//! - Win32 imports are declared via `.extern` — the MinGW linker resolves them. +//! - For imported functions, we use `call [rip+__imp_]` (IAT indirection) +//! to be immune to import-distance relocation issues. + +use crate::codegen::emit::Emitter; +use crate::codegen::platform::{Arch, Platform}; +use crate::codegen_support::RuntimeFeatures; + +mod imports; +mod shims_c_symbols; +mod shims_compress; +mod shims_encoding; +mod shims_errors; +mod shims_fs; +mod shims_misc; +mod shims_net; +mod shims_pcre; +mod shims_time; +#[cfg(test)] +mod tests; + +use imports::*; +use shims_c_symbols::*; +use shims_compress::*; +use shims_encoding::*; +use shims_errors::*; +use shims_fs::*; +use shims_misc::*; +use shims_net::*; +use shims_pcre::*; +use shims_time::*; + +pub(crate) use imports::windows_c_shim_name; +pub(crate) use shims_compress::emit_shim_iconv; + +/// Emits all Win32 shim wrappers for the Windows x86_64 target. +/// +/// Each shim converts SysV calling convention to MSx64 and calls the +/// corresponding Win32 API function. The existing runtime code sets up +/// arguments in SysV registers (rdi, rsi, rdx, r10, r8, r9) before calling +/// these shims — the shims handle the ABI conversion. The zlib/bzip2/pcre2/ +/// iconv third-party shim families are gated on `features` so programs that +/// do not link those libraries never reference their symbols (avoiding an +/// undefined-reference link failure against the base MinGW link set). +pub(crate) fn emit_win32_shims(emitter: &mut Emitter, features: RuntimeFeatures) { + debug_assert_eq!( + (emitter.platform, emitter.target.arch), + (Platform::Windows, Arch::X86_64), + "Win32 shims are only emitted for windows-x86_64" + ); + + emit_win32_imports(emitter); + emit_win32_encoding_helpers(emitter); + emit_win32_error_message(emitter); + emit_native_errno_capture_helpers(emitter); + emit_shim_write(emitter); + emit_shim_read(emitter); + emit_shim_unsupported_syscall(emitter); + emit_shim_exit(emitter); + emit_fiber_stack_overflow_abort(emitter); + emit_shim_sys_init_argv(emitter); + emit_shim_sys_free_argv(emitter); + emit_win_stream_slot_registry(emitter); + emit_win_stream_slot_clear(emitter); + emit_win_stream_mark_timed_out(emitter); + emit_win_stream_clear_timed_out(emitter); + emit_shim_close(emitter); + emit_shim_mmap(emitter); + emit_shim_munmap(emitter); + emit_shim_brk(emitter); + emit_shim_getpid(emitter); + emit_shim_clock_gettime(emitter); + emit_shim_getrandom(emitter); + emit_shim_fstat(emitter); + emit_shim_open(emitter); + emit_shim_lseek(emitter); + emit_win_fd_status_registry(emitter); + emit_is_crt_fd(emitter); + emit_shim_fcntl(emitter); + emit_shim_unlink(emitter); + emit_shim_getcwd(emitter); + emit_shim_chdir(emitter); + emit_shim_mkdir(emitter); + emit_shim_rmdir(emitter); + emit_shim_stat(emitter); + emit_shim_rename(emitter); + emit_shim_chmod(emitter); + emit_shim_getenv(emitter); + emit_shim_putenv(emitter); + emit_shim_tzset(emitter); + emit_shim_time(emitter); + emit_shim_win_safe_gmtime(emitter); + emit_shim_localtime(emitter); + emit_shim_gmtime(emitter); + emit_shim_mktime(emitter); + emit_shim_gettimeofday(emitter); + emit_shim_gethostname(emitter); + emit_shim_strtod(emitter); + emit_shim_strtol(emitter); + emit_shim_gethostbyname(emitter); + emit_shim_snprintf(emitter); + emit_shim_snprintf_double(emitter); + emit_shim_math_fp(emitter); + if features.zlib { + emit_shim_zlib(emitter); + } + if features.bzip2 { + emit_shim_bzip2(emitter); + } + if features.regex { + emit_shim_pcre2_posix(emitter); + } + emit_shim_malloc(emitter); + emit_shim_free(emitter); + emit_unix_loopback_registry(emitter); + emit_shim_socket_shims(emitter); + emit_winsock_init(emitter); + emit_winsock_cleanup(emitter); + emit_win_acl_helpers(emitter); + emit_shim_access(emitter); + emit_shim_ftruncate(emitter); + emit_shim_getrusage(emitter); + emit_shim_ioctl(emitter); + emit_shim_dup_shims(emitter); + emit_shim_getuid_shims(emitter); + emit_shim_kill(emitter); + emit_shim_uname(emitter); + emit_shim_accept4(emitter); + emit_shim_writev(emitter); + emit_shim_sysinfo(emitter); + emit_shim_execve(emitter); + emit_shim_futex(emitter); + emit_shim_mprotect(emitter); + emit_shim_setsockopt(emitter); + emit_shim_getsockopt(emitter); + emit_shim_c_symbols(emitter); + emit_shim_c_symbol_delegates(emitter); + emit_shim_socketpair(emitter); + emit_shim_statfs(emitter); + emit_shim_sys_get_temp_dir(emitter); + emit_shim_pselect6(emitter); + emit_shim_sendmsg(emitter); + emit_shim_recvmsg(emitter); + emit_shim_getdents(emitter); + emit_shim_creat(emitter); + emit_shim_clock_getres(emitter); + emit_shim_newfstatat(emitter); + emit_shim_net_dns(emitter); + if features.iconv { + emit_shim_iconv(emitter); + } + emit_shim_msvcrt_passwd_lookup(emitter); + emit_shim_opendir(emitter); + emit_shim_readdir(emitter); + emit_shim_closedir(emitter); + emit_shim_rewinddir(emitter); + emit_shim_mkstemp(emitter); +} + +/// Emits the fd-to-HANDLE conversion helper. +/// +/// All file descriptors exposed by the runtime, including the CRT-provided +/// standard descriptors, belong to the Microsoft CRT descriptor table. +/// `_get_osfhandle` recovers the underlying Win32 handle for direct API calls. +pub(crate) fn emit_fd_to_handle(emitter: &mut Emitter) { + emitter.label_global("__rt_fd_to_handle"); + emitter.instruction("sub rsp, 40"); // shadow space + emitter.instruction("mov ecx, edi"); // CRT file descriptor + emitter.instruction("call _get_osfhandle"); // recover the owned Win32 HANDLE + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return handle + emitter.blank(); +} + +/// Emits a bounded CRT-descriptor predicate for runtime paths that index +/// fixed per-fd tables. Raw Winsock SOCKET values are opaque pointer-sized +/// handles and must never be used as table offsets. +pub(crate) fn emit_is_crt_fd(emitter: &mut Emitter) { + emitter.label_global("__rt_win_is_crt_fd"); + emitter.instruction("sub rsp, 40"); // reserve MSx64 shadow space + emitter.instruction("mov ecx, edi"); // pass the candidate descriptor to the CRT + emitter.instruction("call _get_osfhandle"); // recover a HANDLE only for CRT descriptors + emitter.instruction("cmp rax, -1"); // CRT rejects raw Winsock SOCKET values + emitter.instruction("setne al"); // return whether the descriptor belongs to the CRT table + emitter.instruction("movzx eax, al"); // widen the boolean result + emitter.instruction("add rsp, 40"); // release shadow space + emitter.instruction("ret"); // return 1 for CRT fd, 0 for raw SOCKET + emitter.blank(); +} + +/// Emits the Windows entry point wrapper. +/// +/// MinGW's CRT startup calls `main(argc, argv, envp)` with MSx64 ABI: +/// rcx=argc, rdx=argv, r8=envp. Our codegen expects SysV ABI: +/// rdi=argc, rsi=argv. This wrapper shuffles the arguments. +pub(crate) fn emit_main_wrapper(emitter: &mut Emitter) { + emitter.label_global("main"); + emitter.instruction("sub rsp, 24"); // align stack to 16 bytes + spill slots for argc/argv + emitter.instruction("mov QWORD PTR [rsp + 0], rcx"); // spill argc (rcx is volatile on MSx64) across the init call + emitter.instruction("mov QWORD PTR [rsp + 8], rdx"); // spill argv (rdx is volatile on MSx64) across the init call + // -- initialize Winsock before any socket use -- + emitter.instruction("call __rt_winsock_init"); // WSAStartup(MAKEWORD(2,2), &wsadata) — idempotent across re-entry + emitter.instruction("mov rdi, QWORD PTR [rsp + 0]"); // SysV arg1 = argc (reloaded after the init call) + emitter.instruction("mov rsi, QWORD PTR [rsp + 8]"); // SysV arg2 = argv (reloaded after the init call) + emitter.instruction("call __elephc_main"); // call the real program entry + emitter.instruction("mov QWORD PTR [rsp + 16], rax"); // preserve program status across Winsock cleanup + emitter.instruction("call __rt_winsock_cleanup"); // balance WSAStartup on normal program return + emitter.instruction("mov rax, QWORD PTR [rsp + 16]"); // restore program status for the CRT + emitter.instruction("add rsp, 24"); // restore stack + emitter.instruction("ret"); // return to CRT + emitter.blank(); +} diff --git a/src/codegen_support/runtime/win32/shims_c_symbols.rs b/src/codegen_support/runtime/win32/shims_c_symbols.rs new file mode 100644 index 0000000000..7fdb2b6228 --- /dev/null +++ b/src/codegen_support/runtime/win32/shims_c_symbols.rs @@ -0,0 +1,2360 @@ +//! Win32 shims for the C-symbol delegate family: the 762-LOC `c_symbols` +//! cohesive emitter, its stub delegates, and the msvcrt-real +//! passwd/group-lookup shims (fopen/fgets/fclose/strncmp/strchr/strtoul). + +use crate::codegen::emit::Emitter; + +/// Emits shim wrappers for syscalls that have C symbol stubs but no `__rt_sys_*` label. +/// +/// `windows_transform.rs` maps Linux syscalls 86/88/89/92/93/94 to `__rt_sys_*` names, +/// but the actual implementations live as C symbol stubs (link, symlink, readlink, etc.). +/// These thin wrappers shuffle SysV args to match the C stub calling convention and +/// delegate to the stubs. +pub(super) fn emit_shim_c_symbol_delegates(emitter: &mut Emitter) { + // __rt_sys_link: delegate to C symbol `link` (CreateHardLinkW) + // SysV: rdi=oldpath, rsi=newpath → C stub expects same SysV args + emitter.label_global("__rt_sys_link"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call link"); // delegate to C symbol stub + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // __rt_sys_symlink: delegate to C symbol `symlink` (CreateSymbolicLinkW) + // SysV: rdi=target, rsi=linkpath → C stub expects same SysV args + emitter.label_global("__rt_sys_symlink"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call symlink"); // delegate to C symbol stub + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // __rt_sys_readlink: delegate to C symbol `readlink` (GetFinalPathNameByHandleW) + // SysV: rdi=path, rsi=buf, rdx=bufsize → C stub expects same SysV args + emitter.label_global("__rt_sys_readlink"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call readlink"); // delegate to C symbol stub + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // __rt_sys_chown: return -1 (ENOSYS — Windows uses ACLs) + emitter.label_global("__rt_sys_chown"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 38"); // ENOSYS: Windows ownership is ACL-based + emitter.instruction("mov rax, -1"); // return -1 (not supported) + emitter.instruction("ret"); // return + emitter.blank(); + + // __rt_sys_fchown: return -1 (ENOSYS) + emitter.label_global("__rt_sys_fchown"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 38"); // ENOSYS: Windows ownership is ACL-based + emitter.instruction("mov rax, -1"); // return -1 (not supported) + emitter.instruction("ret"); // return + emitter.blank(); + + // __rt_sys_lchown: return -1 (ENOSYS) + emitter.label_global("__rt_sys_lchown"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 38"); // ENOSYS: Windows ownership is ACL-based + emitter.instruction("mov rax, -1"); // return -1 (not supported) + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits stubs for C library symbols that are called directly by the runtime +/// but do not exist on Windows. Each stub either delegates to a Win32 equivalent +/// or returns a safe default value. +pub(super) fn emit_shim_c_symbols(emitter: &mut Emitter) { + emitter.raw(" # -- C symbol stubs for functions not available on Windows --"); + + // flock: use a zeroed OVERLAPPED at offset zero and lock the whole remaining file. + // PHP's LOCK_UN is bit 3; LOCK_NB is bit 2 and maps to FAIL_IMMEDIATELY. + emitter.label_global("flock"); + emitter.instruction("sub rsp, 104"); // shadow, stack args, OVERLAPPED, saved handle/op, and error + emitter.instruction("mov QWORD PTR [rsp + 88], rsi"); // preserve operation across Win32 calls + emitter.instruction("mov rcx, rdi"); // fd + emitter.instruction("call __rt_fd_to_handle"); // convert fd to HANDLE + emitter.instruction("cmp rax, -1"); // invalid CRT descriptor? + emitter.instruction("je .Lflock_bad_fd"); // return EBADF + emitter.instruction("mov QWORD PTR [rsp + 80], rax"); // preserve HANDLE across unlock-before-lock + emitter.instruction("pxor xmm0, xmm0"); // zero the complete OVERLAPPED state + emitter.instruction("movdqu XMMWORD PTR [rsp + 48], xmm0"); // OVERLAPPED bytes 0..15 + emitter.instruction("movdqu XMMWORD PTR [rsp + 64], xmm0"); // OVERLAPPED bytes 16..31 + emitter.instruction("test QWORD PTR [rsp + 88], 8"); // LOCK_UN? + emitter.instruction("jnz .Lflock_unlock"); // perform only the explicit unlock + // -- PHP compatibility: discard any prior lock owned by this descriptor before relocking -- + emitter.instruction("mov rcx, QWORD PTR [rsp + 80]"); // handle + emitter.instruction("xor edx, edx"); // dwReserved = 0 + emitter.instruction("mov r8d, 0xFFFFFFFF"); // nNumberOfBytesToUnlockLow = MAXDWORD + emitter.instruction("mov r9d, 0xFFFFFFFF"); // nNumberOfBytesToUnlockHigh = MAXDWORD + emitter.instruction("lea rax, [rsp + 48]"); // stable zero-offset OVERLAPPED + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // fifth MSx64 argument + emitter.instruction("call UnlockFileEx"); // ignore ERROR_NOT_LOCKED before acquiring a new mode + emitter.instruction("mov rcx, QWORD PTR [rsp + 80]"); // handle for LockFileEx + emitter.instruction("xor edx, edx"); // dwFlags = 0 + emitter.instruction("xor r8d, r8d"); // dwReserved = 0 + emitter.instruction("test QWORD PTR [rsp + 88], 2"); // LOCK_EX (bit 1)? + emitter.instruction("jz .Lflock_shared"); // shared lock keeps flags clear + emitter.instruction("or edx, 2"); // LOCKFILE_EXCLUSIVE_LOCK + emitter.label(".Lflock_shared"); + emitter.instruction("test QWORD PTR [rsp + 88], 4"); // LOCK_NB (bit 2)? + emitter.instruction("jz .Lflock_no_nb"); // skip if not LOCK_NB + emitter.instruction("or edx, 1"); // LOCKFILE_FAIL_IMMEDIATELY + emitter.label(".Lflock_no_nb"); + emitter.instruction("mov r9d, 0xFFFFFFFF"); // nNumberOfBytesToLockLow = MAXDWORD (zero-extends to r9) + emitter.instruction("mov DWORD PTR [rsp + 32], 0xFFFFFFFF"); // nNumberOfBytesToLockHigh = MAXDWORD + emitter.instruction("lea rax, [rsp + 48]"); // zero-offset OVERLAPPED + emitter.instruction("mov QWORD PTR [rsp + 40], rax"); // sixth MSx64 argument + emitter.instruction("call LockFileEx"); // lock file + emitter.instruction("jmp .Lflock_finish"); // normalize BOOL result to POSIX convention + emitter.label(".Lflock_unlock"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 80]"); // handle + emitter.instruction("xor rdx, rdx"); // dwReserved = 0 + emitter.instruction("mov r8d, 0xFFFFFFFF"); // nNumberOfBytesToUnlockLow = MAXDWORD (zero-extends) + emitter.instruction("mov r9d, 0xFFFFFFFF"); // nNumberOfBytesToUnlockHigh = MAXDWORD (zero-extends) + emitter.instruction("lea rax, [rsp + 48]"); // zero-offset OVERLAPPED + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // fifth MSx64 argument + emitter.instruction("call UnlockFileEx"); // unlock file + emitter.label(".Lflock_finish"); + emitter.instruction("test eax, eax"); // Win32 BOOL success? + emitter.instruction("jz .Lflock_fail"); // publish errno and return -1 + emitter.instruction("xor rax, rax"); // return 0 + emitter.instruction("add rsp, 104"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lflock_fail"); + emitter.instruction("call GetLastError"); // capture lock failure + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // retain native diagnostic + emitter.instruction("cmp eax, 32"); // ERROR_SHARING_VIOLATION? + emitter.instruction("je .Lflock_would_block"); // PHP reports nonblocking contention via would_block + emitter.instruction("cmp eax, 33"); // ERROR_LOCK_VIOLATION? + emitter.instruction("je .Lflock_would_block"); // same contention result + emitter.instruction("call __rt_win32_errno_from_code"); // translate other failures + emitter.instruction("jmp .Lflock_store_errno"); // publish translated errno + emitter.label(".Lflock_would_block"); + emitter.instruction("mov eax, 11"); // EAGAIN / EWOULDBLOCK + emitter.label(".Lflock_store_errno"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish POSIX errno + emitter.instruction("mov rax, -1"); // POSIX flock failure + emitter.instruction("add rsp, 104"); // restore stack + emitter.instruction("ret"); // return failure + emitter.label(".Lflock_bad_fd"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 9"); // EBADF + emitter.instruction("mov rax, -1"); // invalid descriptor + emitter.instruction("add rsp, 104"); // restore stack + emitter.instruction("ret"); // return failure + emitter.blank(); + + // __errno_location: return pointer to thread-local errno + // On Windows, msvcrt provides _errno() which returns the same thing. + // We alias it to a static errno variable. Written on failure by the + // __rt_sys_read (ReadFile) shim (shims_fs.rs) and the recvfrom/recvmsg + // (Winsock) shims (shims_net.rs) via __rt_win32_errno_from_code below; + // left unchanged on success, matching POSIX errno semantics. + emitter.label_global("__errno_location"); + emitter.instruction("lea rax, [rip + __rt_errno]"); // return pointer to static errno + emitter.instruction("ret"); // return + emitter.blank(); + + // symlink: strict wide conversion with unprivileged-create retry + // SysV: rdi=target, rsi=linkpath → Win32: rcx=symlinkPath, rdx=targetPath + emitter.label_global("symlink"); + emitter.instruction("sub rsp, 72"); // shadow space and two owned wide paths + emitter.instruction("mov QWORD PTR [rsp + 48], rsi"); // preserve UTF-8 link path + emitter.instruction("call __rt_win_utf8_to_utf16"); // convert target + emitter.instruction("test rax, rax"); // target conversion succeeded? + emitter.instruction("jz .Lsymlink_conversion_fail"); // invalid UTF-8 + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // owned wide target + emitter.instruction("mov rdi, QWORD PTR [rsp + 48]"); // UTF-8 link path + emitter.instruction("call __rt_win_utf8_to_utf16"); // convert link path + emitter.instruction("test rax, rax"); // link conversion succeeded? + emitter.instruction("jz .Lsymlink_link_conversion_fail"); // cleanup target + emitter.instruction("mov QWORD PTR [rsp + 40], rax"); // owned wide link path + emitter.instruction("mov rcx, QWORD PTR [rsp + 32]"); // wide target for directory detection + emitter.instruction("call GetFileAttributesW"); // detect directory reparse target when it exists + emitter.instruction("mov r8, 2"); // ALLOW_UNPRIVILEGED_CREATE + emitter.instruction("cmp eax, -1"); // target attributes available? + emitter.instruction("je .Lsymlink_flags_ready"); // dangling target: keep file-link semantics + emitter.instruction("test eax, 0x10"); // FILE_ATTRIBUTE_DIRECTORY? + emitter.instruction("jz .Lsymlink_flags_ready"); // regular-file target + emitter.instruction("or r8, 1"); // SYMBOLIC_LINK_FLAG_DIRECTORY + emitter.label(".Lsymlink_flags_ready"); + emitter.instruction("mov QWORD PTR [rsp + 64], r8"); // preserve directory flag for retry + emitter.instruction("mov rcx, QWORD PTR [rsp + 40]"); // lpSymlinkPath + emitter.instruction("mov rdx, QWORD PTR [rsp + 32]"); // lpTargetPath + emitter.instruction("call CreateSymbolicLinkW"); // create Unicode symbolic link + emitter.instruction("test rax, rax"); // success? + emitter.instruction("jnz .Lsymlink_ok"); // → success + // -- retry without unprivileged flag (requires admin) -- + emitter.instruction("mov rcx, QWORD PTR [rsp + 40]"); // lpSymlinkPath + emitter.instruction("mov rdx, QWORD PTR [rsp + 32]"); // lpTargetPath + emitter.instruction("mov r8, QWORD PTR [rsp + 64]"); // preserve directory-link classification + emitter.instruction("and r8, 1"); // retry without unprivileged bit + emitter.instruction("call CreateSymbolicLinkW"); // retry without unprivileged flag + emitter.instruction("test rax, rax"); // success? + emitter.instruction("jz .Lsymlink_fail"); // → failure + emitter.label(".Lsymlink_ok"); + emitter.instruction("mov rax, QWORD PTR [rsp + 40]"); // owned wide link path + emitter.instruction("call __rt_heap_free"); // release link conversion + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // owned wide target + emitter.instruction("call __rt_heap_free"); // release target conversion + emitter.instruction("xor rax, rax"); // return 0 (success) + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lsymlink_fail"); + emitter.instruction("call GetLastError"); // capture final native failure + emitter.instruction("mov DWORD PTR [rsp + 56], eax"); // preserve error across cleanup + emitter.instruction("mov rax, QWORD PTR [rsp + 40]"); // owned wide link path + emitter.instruction("call __rt_heap_free"); // release link conversion + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // owned wide target + emitter.instruction("call __rt_heap_free"); // release target conversion + emitter.instruction("mov eax, DWORD PTR [rsp + 56]"); // restore native error + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // retain native state + emitter.instruction("call __rt_win32_errno_from_code"); // translate errno + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish errno + emitter.instruction("mov rax, -1"); // return -1 on failure + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lsymlink_link_conversion_fail"); + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // owned wide target + emitter.instruction("call __rt_heap_free"); // release target conversion + emitter.label(".Lsymlink_conversion_fail"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 84"); // EILSEQ + emitter.instruction("mov rax, -1"); // return failure + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // link: delegate to CreateHardLinkW (args reversed from POSIX), then translate + // the Win32 BOOL result (nonzero = success) to the POSIX convention __rt_link + // expects (0 = success, -1 = failure) — mirrors the symlink shim immediately + // above. Without this translation, CreateHardLinkW success (nonzero) compared + // against 0 reports failure, and vice versa. + emitter.label_global("link"); + emitter.instruction("sub rsp, 72"); // shadow space and two owned wide paths + emitter.instruction("mov QWORD PTR [rsp + 48], rsi"); // preserve UTF-8 new path + emitter.instruction("call __rt_win_utf8_to_utf16"); // convert existing path + emitter.instruction("test rax, rax"); // conversion succeeded? + emitter.instruction("jz .Llink_conversion_fail"); // invalid UTF-8 + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // owned wide existing path + emitter.instruction("mov rdi, QWORD PTR [rsp + 48]"); // UTF-8 new path + emitter.instruction("call __rt_win_utf8_to_utf16"); // convert new path + emitter.instruction("test rax, rax"); // conversion succeeded? + emitter.instruction("jz .Llink_new_conversion_fail"); // cleanup existing path + emitter.instruction("mov QWORD PTR [rsp + 40], rax"); // owned wide new path + emitter.instruction("mov rcx, rax"); // lpFileName = new path + emitter.instruction("mov rdx, QWORD PTR [rsp + 32]"); // lpExistingFileName + emitter.instruction("xor r8, r8"); // lpSecurityAttributes = NULL + emitter.instruction("call CreateHardLinkW"); // create Unicode hard link + emitter.instruction("test eax, eax"); // Win32 BOOL: nonzero = success + emitter.instruction("jz .Llink_fail"); // zero = failure + emitter.instruction("mov rax, QWORD PTR [rsp + 40]"); // owned wide new path + emitter.instruction("call __rt_heap_free"); // release new path + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // owned wide existing path + emitter.instruction("call __rt_heap_free"); // release existing path + emitter.instruction("xor rax, rax"); // translate success to POSIX 0 + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Llink_fail"); + emitter.instruction("call GetLastError"); // capture native failure + emitter.instruction("mov DWORD PTR [rsp + 56], eax"); // preserve native error + emitter.instruction("mov rax, QWORD PTR [rsp + 40]"); // owned wide new path + emitter.instruction("call __rt_heap_free"); // release new path + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // owned wide existing path + emitter.instruction("call __rt_heap_free"); // release existing path + emitter.instruction("mov eax, DWORD PTR [rsp + 56]"); // restore native error + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // retain native state + emitter.instruction("call __rt_win32_errno_from_code"); // translate errno + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish errno + emitter.instruction("mov rax, -1"); // translate failure to POSIX -1 + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Llink_new_conversion_fail"); + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // owned wide existing path + emitter.instruction("call __rt_heap_free"); // release existing path + emitter.label(".Llink_conversion_fail"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 84"); // EILSEQ + emitter.instruction("mov rax, -1"); // return failure + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + emit_shim_readlink_reparse(emitter); + emit_shim_lstat_symlink_probe(emitter); + + // Legacy final-path implementation retained as a non-routed diagnostic reference. + if false { + emitter.label_global("__rt_readlink_final_path_legacy"); + emitter.instruction("sub rsp, 104"); // CreateFile stack args and owned path/output locals + emitter.instruction("mov QWORD PTR [rsp + 80], rsi"); // preserve caller UTF-8 buffer + emitter.instruction("mov QWORD PTR [rsp + 88], rdx"); // preserve caller byte capacity + emitter.instruction("call __rt_win_utf8_to_utf16"); // convert link path strictly + emitter.instruction("test rax, rax"); // path conversion succeeded? + emitter.instruction("jz .Lreadlink_conversion_fail"); // invalid UTF-8 + emitter.instruction("mov QWORD PTR [rsp + 56], rax"); // owned wide input path + emitter.instruction("mov rax, 65536"); // 32,768 WCHAR output buffer + emitter.instruction("call __rt_heap_alloc"); // allocate long-path wide output + emitter.instruction("test rax, rax"); // allocation succeeded? + emitter.instruction("jz .Lreadlink_alloc_fail"); // cleanup input and return ENOMEM + emitter.instruction("mov QWORD PTR [rsp + 64], rax"); // owned wide output + emitter.instruction("mov rcx, QWORD PTR [rsp + 56]"); // Unicode link path + emitter.instruction("xor edx, edx"); // metadata-only access + emitter.instruction("mov r8, 7"); // share read/write/delete + emitter.instruction("xor r9, r9"); // security attributes = NULL + emitter.instruction("mov QWORD PTR [rsp + 32], 3"); // OPEN_EXISTING + emitter.instruction("mov QWORD PTR [rsp + 40], 0x2200000"); // BACKUP_SEMANTICS | OPEN_REPARSE_POINT + emitter.instruction("mov QWORD PTR [rsp + 48], 0"); // template = NULL + emitter.instruction("call CreateFileW"); // open the reparse point without following it + emitter.instruction("cmp rax, -1"); // INVALID_HANDLE_VALUE? + emitter.instruction("je .Lreadlink_native_fail"); // capture open failure + emitter.instruction("mov QWORD PTR [rsp + 72], rax"); // retain handle + emitter.instruction("mov rcx, rax"); // reparse-point handle + emitter.instruction("mov rdx, QWORD PTR [rsp + 64]"); // wide output buffer + emitter.instruction("mov r8, 32768"); // WCHAR capacity + emitter.instruction("mov r9, 8"); // FILE_NAME_OPENED preserves reparse-point naming + emitter.instruction("call GetFinalPathNameByHandleW"); // retrieve long final/opened path + emitter.instruction("mov DWORD PTR [rsp + 96], eax"); // preserve WCHAR length/status + emitter.instruction("test eax, eax"); // final-path query succeeded? + emitter.instruction("jnz .Lreadlink_query_ok"); // close and process successful result + emitter.instruction("call GetLastError"); // capture query failure before CloseHandle + emitter.instruction("mov DWORD PTR [rsp + 96], eax"); // preserve native error across close + emitter.instruction("mov rcx, QWORD PTR [rsp + 72]"); // opened handle + emitter.instruction("call CloseHandle"); // close on every query result + emitter.instruction("jmp .Lreadlink_native_fail_saved"); // cleanup with saved query error + emitter.label(".Lreadlink_query_ok"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 72]"); // opened handle + emitter.instruction("call CloseHandle"); // close successful query handle + emitter.instruction("cmp DWORD PTR [rsp + 96], 32768"); // result exceeded WCHAR capacity? + emitter.instruction("jae .Lreadlink_range_fail"); // reject truncated long path + emitter.instruction("mov rdi, QWORD PTR [rsp + 64]"); // wide result base + emitter.instruction("mov rax, QWORD PTR [rdi]"); // first four UTF-16 code units + emitter.instruction("mov r10, 0x005c003f005c005c"); // UTF-16 `\\?\` prefix + emitter.instruction("cmp rax, r10"); // extended path prefix? + emitter.instruction("jne .Lreadlink_prefix_done"); // keep ordinary path + emitter.instruction("mov rax, QWORD PTR [rdi + 8]"); // code units four through seven + emitter.instruction("mov r10, 0x005c0043004e0055"); // UTF-16 `UNC\` + emitter.instruction("cmp rax, r10"); // extended UNC prefix? + emitter.instruction("jne .Lreadlink_drive_prefix"); // ordinary extended drive path + emitter.instruction("mov WORD PTR [rdi + 12], 92"); // synthesize first UNC slash at old C position + emitter.instruction("mov WORD PTR [rdi + 14], 92"); // synthesize second UNC slash + emitter.instruction("add rdi, 12"); // source now begins `\\server` + emitter.instruction("jmp .Lreadlink_prefix_done"); // normalized UNC source + emitter.label(".Lreadlink_drive_prefix"); + emitter.instruction("add rdi, 8"); // strip four-WCHAR `\\?\` prefix + emitter.label(".Lreadlink_prefix_done"); + emitter.instruction("mov rsi, QWORD PTR [rsp + 80]"); // caller UTF-8 output + emitter.instruction("mov rdx, QWORD PTR [rsp + 88]"); // output byte capacity + emitter.instruction("call __rt_win_utf16_to_utf8"); // strict conversion including NUL + emitter.instruction("test eax, eax"); // conversion succeeded and fit? + emitter.instruction("jz .Lreadlink_native_fail"); // capture conversion/range failure + emitter.instruction("dec eax"); // readlink returns payload bytes excluding NUL + emitter.instruction("mov DWORD PTR [rsp + 96], eax"); // preserve result length across cleanup + emitter.instruction("jmp .Lreadlink_cleanup_success"); // free both owned buffers + emitter.label(".Lreadlink_cleanup_success"); + emitter.instruction("mov rax, QWORD PTR [rsp + 64]"); // owned wide output + emitter.instruction("call __rt_heap_free"); // release output buffer + emitter.instruction("mov rax, QWORD PTR [rsp + 56]"); // owned wide input + emitter.instruction("call __rt_heap_free"); // release input conversion + emitter.instruction("mov eax, DWORD PTR [rsp + 96]"); // return UTF-8 payload length + emitter.instruction("add rsp, 104"); // restore stack + emitter.instruction("ret"); // return bytes written + emitter.label(".Lreadlink_native_fail"); + emitter.instruction("call GetLastError"); // capture native/conversion failure + emitter.instruction("mov DWORD PTR [rsp + 96], eax"); // preserve error across cleanup + emitter.label(".Lreadlink_native_fail_saved"); + emitter.instruction("mov rax, QWORD PTR [rsp + 64]"); // owned wide output + emitter.instruction("call __rt_heap_free"); // release output buffer + emitter.instruction("mov rax, QWORD PTR [rsp + 56]"); // owned wide input + emitter.instruction("call __rt_heap_free"); // release input path + emitter.instruction("mov eax, DWORD PTR [rsp + 96]"); // restore native error + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // retain native state + emitter.instruction("call __rt_win32_errno_from_code"); // translate errno + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish errno + emitter.instruction("mov rax, -1"); // return failure + emitter.instruction("add rsp, 104"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lreadlink_range_fail"); + emitter.instruction("mov rax, QWORD PTR [rsp + 64]"); // owned wide output + emitter.instruction("call __rt_heap_free"); // release output buffer + emitter.instruction("mov rax, QWORD PTR [rsp + 56]"); // owned wide input + emitter.instruction("call __rt_heap_free"); // release input path + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 36"); // ENAMETOOLONG + emitter.instruction("mov rax, -1"); // return failure + emitter.instruction("add rsp, 104"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lreadlink_alloc_fail"); + emitter.instruction("mov rax, QWORD PTR [rsp + 56]"); // owned wide input + emitter.instruction("call __rt_heap_free"); // release input conversion + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 12"); // ENOMEM + emitter.instruction("mov rax, -1"); // return failure + emitter.instruction("add rsp, 104"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lreadlink_conversion_fail"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 84"); // EILSEQ + emitter.instruction("mov rax, -1"); // return failure + emitter.instruction("add rsp, 104"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + } + + // lstat: probe the reparse point before stat() so dangling symbolic links succeed. + let lstat_mode_off = emitter.platform.stat_mode_offset(); + let lstat_size_off = emitter.platform.stat_size_offset(); + let lstat_nlink_off = emitter.platform.stat_nlink_offset(); + let lstat_ino_off = emitter.platform.stat_ino_offset(); + let lstat_dev_off = emitter.platform.stat_dev_offset(); + let lstat_atime_off = emitter.platform.stat_atime_offset(); + let lstat_mtime_off = emitter.platform.stat_mtime_offset(); + let lstat_ctime_off = emitter.platform.stat_ctime_offset(); + emitter.label_global("lstat"); + emitter.instruction("sub rsp, 168"); // shadow, CreateFile args, metadata, and owned path/error locals aligned for MSx64 + emitter.instruction("mov QWORD PTR [rsp + 144], rdi"); // preserve the UTF-8 path across the tag probe + emitter.instruction("mov QWORD PTR [rsp + 64], rsi"); // preserve the Linux-layout stat destination + emitter.instruction("call __rt_lstat_is_symlink"); // inspect the exact reparse tag without following its target + emitter.instruction("test eax, eax"); // tri-state: 1 = symlink, 0 = normal/junction, -1 = reparse query failure + emitter.instruction("js .Llstat_probe_fail"); // php-src propagates FSCTL_GET_REPARSE_POINT failure instead of falling back to stat + emitter.instruction("jz .Llstat_followed_fallback"); // junctions and ordinary paths retain normal stat semantics + // -- symbolic link: reopen the reparse point itself, including a dangling target -- + emitter.instruction("mov rdi, QWORD PTR [rsp + 144]"); // restore the original UTF-8 link path + emitter.instruction("call __rt_win_utf8_to_utf16"); // convert the link name for CreateFileW + emitter.instruction("test rax, rax"); // conversion succeeded? + emitter.instruction("jz .Llstat_conversion_fail"); // helper already published EILSEQ + emitter.instruction("mov QWORD PTR [rsp + 56], rax"); // retain owned UTF-16 path through metadata collection + emitter.instruction("mov rcx, rax"); // lpFileName = wide link path + emitter.instruction("xor edx, edx"); // dwDesiredAccess = metadata-only + emitter.instruction("mov r8, 7"); // dwShareMode = FILE_SHARE_READ|WRITE|DELETE + emitter.instruction("xor r9, r9"); // lpSecurityAttributes = NULL + emitter.instruction("mov QWORD PTR [rsp + 32], 3"); // dwCreationDisposition = OPEN_EXISTING + emitter.instruction("mov QWORD PTR [rsp + 40], 0x2200000"); // BACKUP_SEMANTICS | OPEN_REPARSE_POINT: do not follow the link + emitter.instruction("mov QWORD PTR [rsp + 48], 0"); // hTemplateFile = NULL + emitter.instruction("call CreateFileW"); // open the symbolic-link object even when its target is missing + emitter.instruction("cmp rax, -1"); // INVALID_HANDLE_VALUE? + emitter.instruction("je .Llstat_native_fail"); // preserve the open error through cleanup + emitter.instruction("mov QWORD PTR [rsp + 72], rax"); // retain the reparse-point handle + emitter.instruction("cld"); // forward direction for rep stosb + emitter.instruction("lea rdi, [rsp + 80]"); // destination = BY_HANDLE_FILE_INFORMATION buffer + emitter.instruction("xor eax, eax"); // zero fill byte + emitter.instruction("mov ecx, 52"); // sizeof(BY_HANDLE_FILE_INFORMATION) + emitter.instruction("rep stosb"); // clear the complete metadata buffer + emitter.instruction("mov rcx, QWORD PTR [rsp + 72]"); // hFile = opened reparse point + emitter.instruction("lea rdx, [rsp + 80]"); // lpFileInformation = stack metadata buffer + emitter.instruction("call GetFileInformationByHandle"); // read link-object size, file index, times, and attributes + emitter.instruction("test eax, eax"); // metadata query succeeded? + emitter.instruction("jnz .Llstat_metadata_ready"); // close then populate the stat buffer + emitter.instruction("call GetLastError"); // capture metadata failure before CloseHandle + emitter.instruction("mov DWORD PTR [rsp + 136], eax"); // preserve native error across cleanup + emitter.instruction("mov rcx, QWORD PTR [rsp + 72]"); // opened reparse-point handle + emitter.instruction("call CloseHandle"); // close even when metadata collection failed + emitter.instruction("jmp .Llstat_native_fail_saved"); // release UTF-16 path and translate saved error + emitter.label(".Llstat_metadata_ready"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 72]"); // opened reparse-point handle + emitter.instruction("call CloseHandle"); // close before constructing the caller-visible stat buffer + // -- fill the runtime's Linux-layout stat buffer from the link object's metadata -- + emitter.instruction("cld"); // forward direction for rep stosb + emitter.instruction("mov rsi, QWORD PTR [rsp + 64]"); // restore caller stat destination + emitter.instruction("mov rdi, rsi"); // destination = stat buffer base + emitter.instruction("xor eax, eax"); // zero fill byte + emitter.instruction("mov ecx, 128"); // Windows runtime uses the Linux-sized stat layout + emitter.instruction("rep stosb"); // clear unspecified POSIX fields deterministically + emitter.instruction("mov eax, DWORD PTR [rsp + 116]"); // nFileSizeLow + emitter.instruction("mov edx, DWORD PTR [rsp + 112]"); // nFileSizeHigh + emitter.instruction("shl rdx, 32"); // position the high size dword + emitter.instruction("or rax, rdx"); // combine the 64-bit link-object size + emitter.instruction(&format!("mov QWORD PTR [rsi + {}], rax", lstat_size_off)); // store st_size without following the target + emitter.instruction("mov eax, DWORD PTR [rsp + 108]"); // dwVolumeSerialNumber + emitter.instruction("test eax, eax"); // synthetic Wine volumes may report zero + emitter.instruction("jnz .Llstat_dev_ready"); // retain a real native volume id + emitter.instruction("mov eax, 1"); // stable nonzero device fallback + emitter.label(".Llstat_dev_ready"); + emitter.instruction(&format!("mov QWORD PTR [rsi + {}], rax", lstat_dev_off)); // store st_dev + emitter.instruction("mov eax, DWORD PTR [rsp + 124]"); // nFileIndexHigh + emitter.instruction("shl rax, 32"); // position the inode high dword + emitter.instruction("mov ecx, DWORD PTR [rsp + 128]"); // nFileIndexLow + emitter.instruction("or rax, rcx"); // combine the stable link-object inode number + emitter.instruction(&format!("mov QWORD PTR [rsi + {}], rax", lstat_ino_off)); // store st_ino + emitter.instruction("mov eax, DWORD PTR [rsp + 120]"); // nNumberOfLinks + emitter.instruction("test eax, eax"); // native link count is present? + emitter.instruction("jnz .Llstat_nlink_ready"); // retain a real nonzero count + emitter.instruction("mov eax, 1"); // POSIX-compatible minimum link count + emitter.label(".Llstat_nlink_ready"); + emitter.instruction(&format!("mov DWORD PTR [rsi + {}], eax", lstat_nlink_off)); // store st_nlink + emitter.instruction("mov eax, DWORD PTR [rsp + 80]"); // link-object dwFileAttributes + emitter.instruction("test eax, 1"); // FILE_ATTRIBUTE_READONLY? + emitter.instruction("jnz .Llstat_mode_readonly"); // omit write bits for read-only links + emitter.instruction("mov eax, 0xA1B6"); // S_IFLNK | 0666 + emitter.instruction("jmp .Llstat_mode_ready"); // store the writable-link mode + emitter.label(".Llstat_mode_readonly"); + emitter.instruction("mov eax, 0xA124"); // S_IFLNK | 0444 + emitter.label(".Llstat_mode_ready"); + emitter.instruction(&format!("mov DWORD PTR [rsi + {}], eax", lstat_mode_off)); // store php-src's Windows symlink permissions + // -- convert the link object's FILETIME values to Unix epoch seconds -- + emit_lstat_filetime_seconds(emitter, 92, lstat_atime_off); + emit_lstat_filetime_seconds(emitter, 100, lstat_mtime_off); + emit_lstat_filetime_seconds(emitter, 84, lstat_ctime_off); + emitter.instruction("mov rax, QWORD PTR [rsp + 56]"); // owned UTF-16 link path + emitter.instruction("call __rt_heap_free"); // release conversion after all metadata is consumed + emitter.instruction("xor eax, eax"); // lstat succeeded for the symbolic-link object + emitter.instruction("add rsp, 168"); // restore the SysV caller stack + emitter.instruction("ret"); // return success + emitter.label(".Llstat_followed_fallback"); + emitter.instruction("mov rdi, QWORD PTR [rsp + 144]"); // original UTF-8 path for normal stat + emitter.instruction("mov rsi, QWORD PTR [rsp + 64]"); // original stat destination + emitter.instruction("call __rt_sys_stat"); // follow junctions/non-reparse paths exactly as stat does + emitter.instruction("add rsp, 168"); // restore the SysV caller stack + emitter.instruction("ret"); // propagate ordinary stat status + emitter.label(".Llstat_probe_fail"); + emitter.instruction("mov rax, -1"); // probe already published the corresponding POSIX errno + emitter.instruction("add rsp, 168"); // restore the SysV caller stack without invoking followed stat + emitter.instruction("ret"); // propagate the exact reparse-control failure + emitter.label(".Llstat_native_fail"); + emitter.instruction("call GetLastError"); // capture CreateFileW failure before freeing the path + emitter.instruction("mov DWORD PTR [rsp + 136], eax"); // preserve native error across cleanup + emitter.label(".Llstat_native_fail_saved"); + emitter.instruction("mov rax, QWORD PTR [rsp + 56]"); // owned UTF-16 link path + emitter.instruction("call __rt_heap_free"); // release conversion on native failure + emitter.instruction("mov eax, DWORD PTR [rsp + 136]"); // restore native error code + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // retain diagnostic state + emitter.instruction("call __rt_win32_errno_from_code"); // translate native status to POSIX errno + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish observable errno + emitter.instruction("mov rax, -1"); // report lstat failure + emitter.instruction("add rsp, 168"); // restore the SysV caller stack + emitter.instruction("ret"); // return failure + emitter.label(".Llstat_conversion_fail"); + emitter.instruction("mov rax, -1"); // UTF conversion helper already set EILSEQ + emitter.instruction("add rsp, 168"); // restore the SysV caller stack + emitter.instruction("ret"); // return status + emitter.blank(); + + // mmap: delegate to VirtualAlloc via __rt_sys_mmap + emitter.label_global("mmap"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_mmap"); // call VirtualAlloc shim + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // munmap: delegate to VirtualFree via __rt_sys_munmap + emitter.label_global("munmap"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_munmap"); // call VirtualFree shim + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // mprotect: delegate to VirtualProtect via __rt_sys_mprotect + emitter.label_global("mprotect"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_mprotect"); // call VirtualProtect shim + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // brk: delegate to HeapAlloc via __rt_sys_brk + emitter.label_global("brk"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_brk"); // call HeapAlloc shim + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // getrandom: delegate to BCryptGenRandom + emitter.label_global("getrandom"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_getrandom"); // call BCryptGenRandom shim + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // write: delegate to __rt_sys_write + emitter.label_global("write"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_write"); // call WriteFile shim + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // read: delegate to __rt_sys_read + emitter.label_global("read"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_read"); // call ReadFile shim + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // close: delegate to __rt_sys_close + emitter.label_global("close"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_close"); // call CloseHandle shim + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // exit: delegate to __rt_sys_exit + emitter.label_global("exit"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_exit"); // call ExitProcess shim + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // unreachable + emitter.blank(); + + // open: delegate to __rt_sys_open (CreateFileW) + emitter.label_global("open"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_open"); // call CreateFileW shim + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // fstat: delegate to msvcrt fstat + emitter.label_global("fstat"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_fstat"); // call msvcrt fstat + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // lseek: delegate to SetFilePointerEx + emitter.label_global("lseek"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_lseek"); // call SetFilePointerEx shim + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // fcntl: delegate to ioctlsocket for socket operations + emitter.label_global("fcntl"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_ioctl"); // call ioctlsocket shim + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // ioctl: delegate to ioctlsocket + emitter.label_global("ioctl"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_ioctl"); // call ioctlsocket shim + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // getpid: delegate to GetCurrentProcessId + emitter.label_global("getpid"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_getpid"); // call GetCurrentProcessId shim + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // getuid/getgid: return 0 on Windows (PHP behavior — no Unix UID/GID) + for sym in &["getuid", "getgid"] { + emitter.label_global(sym); + emitter.instruction("xor rax, rax"); // return 0 (PHP behavior on Windows) + emitter.instruction("ret"); // return + emitter.blank(); + } + + // getppid/setuid/setgid: return -1 (ENOSYS) — POSIX-only functions + for sym in &["getppid", "setuid", "setgid"] { + emitter.label_global(sym); + emitter.instruction("mov rax, -1"); // return -1 (not supported on Windows) + emitter.instruction("ret"); // return + emitter.blank(); + } + + // kill: share the strict Win32 implementation used by the syscall surface + emitter.label_global("kill"); + emitter.instruction("sub rsp, 8"); // align before entering the SysV runtime helper + emitter.instruction("call __rt_sys_kill"); // preserve strict success/failure semantics + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // clock_gettime: delegate to GetSystemTimeAsFileTime + emitter.label_global("clock_gettime"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_clock_gettime"); // call clock_gettime shim + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // accept4: delegate to accept (Windows doesn't have accept4) + emitter.label_global("accept4"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_accept4"); // call accept shim + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // writev: loop over iovec array calling __rt_sys_write for each entry + emitter.label_global("writev"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_writev"); // call writev stub + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // sysinfo: stub + emitter.label_global("sysinfo"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_sysinfo"); // call sysinfo stub + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // uname: delegate to msvcrt + emitter.label_global("uname"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_uname"); // call uname shim + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // execve: delegate to msvcrt _execvp (replaces current process) + emitter.label_global("execve"); + emitter.instruction("sub rsp, 40"); // shadow space + emitter.instruction("mov rcx, rdi"); // path + emitter.instruction("mov rdx, rsi"); // argv + emitter.instruction("call _execvp"); // execute program (replaces process) + emitter.instruction("add rsp, 40"); // restore stack (only reached on failure) + emitter.instruction("ret"); // return -1 on failure (rax from _execvp) + emitter.blank(); + + // futex: stub + emitter.label_global("futex"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_futex"); // call futex stub + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // utimensat: open the file with FILE_WRITE_ATTRIBUTES and apply the requested + // atime/mtime via SetFileTime. Entry (SysV, matching the Linux utimensat ABI + // `__rt_touch` in modify_x86_64.rs uses): rdi=AT_FDCWD (ignored — Windows has + // no dirfd), rsi=path, rdx=timespec[2]* (ts[0]=atime{tv_sec@0,tv_nsec@8}, + // ts[1]=mtime{tv_sec@16,tv_nsec@24}), rcx=flags (ignored). rdx is MSx64 + // volatile and gets reused as CreateFileW's dwDesiredAccess, so the + // timespec[2] pointer is saved to a stack slot before that call; rsi (path) + // needs no saving since it is only read once, before any call, and is itself + // MSx64 non-volatile. + emitter.label_global("utimensat"); + emitter.instruction("sub rsp, 120"); // CreateFile stack args, times, owned wide path, and error locals + emitter.instruction("mov QWORD PTR [rsp + 56], rdx"); // save timespec pointer before rdx becomes CreateFileW access + emitter.instruction("mov rdi, rsi"); // path is SysV argument two + emitter.instruction("call __rt_win_utf8_to_utf16"); // strictly convert target path + emitter.instruction("test rax, rax"); // conversion succeeded? + emitter.instruction("jz .Lutimensat_conversion_fail"); // invalid UTF-8 path + emitter.instruction("mov QWORD PTR [rsp + 88], rax"); // retain owned wide path + emitter.instruction("mov rcx, rax"); // lpFileName = UTF-16 path + emitter.instruction("mov rdx, 0x100"); // dwDesiredAccess = FILE_WRITE_ATTRIBUTES + emitter.instruction("mov r8, 7"); // dwShareMode = FILE_SHARE_READ|WRITE|DELETE + emitter.instruction("xor r9, r9"); // lpSecurityAttributes = NULL + emitter.instruction("mov QWORD PTR [rsp + 32], 3"); // dwCreationDisposition = OPEN_EXISTING + emitter.instruction("mov QWORD PTR [rsp + 40], 0x2000000"); // dwFlagsAndAttributes = FILE_FLAG_BACKUP_SEMANTICS (lets directories open) + emitter.instruction("mov QWORD PTR [rsp + 48], 0"); // hTemplateFile = NULL + emitter.instruction("call CreateFileW"); // open Unicode target for timestamp update + emitter.instruction("cmp rax, -1"); // INVALID_HANDLE_VALUE? + emitter.instruction("je .Lutimensat_fail"); // jump if the file could not be opened + emitter.instruction("mov QWORD PTR [rsp + 64], rax"); // save the handle across the FILETIME setup and SetFileTime call + // -- atime: timespec[0] = {tv_sec@+0, tv_nsec@+8} -- + emitter.instruction("mov rax, QWORD PTR [rsp + 56]"); // reload the timespec[2] pointer + emitter.instruction("mov rdx, QWORD PTR [rax + 8]"); // atime tv_nsec + emitter.instruction("cmp rdx, 0x3FFFFFFF"); // UTIME_NOW sentinel? + emitter.instruction("je .Lutimensat_atime_now"); // → query the current time + emitter.instruction("mov rcx, QWORD PTR [rax]"); // atime tv_sec + emitter.instruction("mov r8, 10000000"); // 100ns intervals per second + emitter.instruction("imul rcx, r8"); // seconds -> 100ns ticks + emitter.instruction("mov r8, 116444736000000000"); // 1970->1601 epoch offset in 100ns units + emitter.instruction("add rcx, r8"); // FILETIME ticks since 1601 + emitter.instruction("mov QWORD PTR [rsp + 72], rcx"); // store the atime FILETIME + emitter.instruction("jmp .Lutimensat_mtime"); // continue with mtime + emitter.label(".Lutimensat_atime_now"); + emitter.instruction("lea rcx, [rsp + 72]"); // lpSystemTimeAsFileTime out-param + emitter.instruction("call GetSystemTimeAsFileTime"); // atime = current time + emitter.label(".Lutimensat_mtime"); + // -- mtime: timespec[1] = {tv_sec@+16, tv_nsec@+24} -- + emitter.instruction("mov rax, QWORD PTR [rsp + 56]"); // reload the timespec[2] pointer + emitter.instruction("mov rdx, QWORD PTR [rax + 24]"); // mtime tv_nsec + emitter.instruction("cmp rdx, 0x3FFFFFFF"); // UTIME_NOW sentinel? + emitter.instruction("je .Lutimensat_mtime_now"); // → query the current time + emitter.instruction("mov rcx, QWORD PTR [rax + 16]"); // mtime tv_sec + emitter.instruction("mov r8, 10000000"); // 100ns intervals per second + emitter.instruction("imul rcx, r8"); // seconds -> 100ns ticks + emitter.instruction("mov r8, 116444736000000000"); // 1970->1601 epoch offset in 100ns units + emitter.instruction("add rcx, r8"); // FILETIME ticks since 1601 + emitter.instruction("mov QWORD PTR [rsp + 80], rcx"); // store the mtime FILETIME + emitter.instruction("jmp .Lutimensat_set"); // proceed to SetFileTime + emitter.label(".Lutimensat_mtime_now"); + emitter.instruction("lea rcx, [rsp + 80]"); // lpSystemTimeAsFileTime out-param + emitter.instruction("call GetSystemTimeAsFileTime"); // mtime = current time + emitter.label(".Lutimensat_set"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 64]"); // hFile = the opened handle + emitter.instruction("xor rdx, rdx"); // lpCreationTime = NULL (leave creation time untouched) + emitter.instruction("lea r8, [rsp + 72]"); // lpLastAccessTime = &atime FILETIME + emitter.instruction("lea r9, [rsp + 80]"); // lpLastWriteTime = &mtime FILETIME + emitter.instruction("call SetFileTime"); // apply the requested access/modify timestamps + emitter.instruction("test eax, eax"); // timestamp update succeeded? + emitter.instruction("jz .Lutimensat_set_fail"); // capture error before closing handle + emitter.instruction("mov rcx, QWORD PTR [rsp + 64]"); // reload the handle + emitter.instruction("call CloseHandle"); // release the handle + emitter.instruction("mov rax, QWORD PTR [rsp + 88]"); // owned wide path + emitter.instruction("call __rt_heap_free"); // release path conversion + emitter.instruction("add rsp, 120"); // restore stack + emitter.instruction("xor rax, rax"); // return 0 + emitter.instruction("ret"); // return + emitter.label(".Lutimensat_set_fail"); + emitter.instruction("call GetLastError"); // capture SetFileTime failure + emitter.instruction("mov DWORD PTR [rsp + 96], eax"); // preserve error across close + emitter.instruction("mov rcx, QWORD PTR [rsp + 64]"); // opened handle + emitter.instruction("call CloseHandle"); // close failed update handle + emitter.instruction("jmp .Lutimensat_fail_saved"); // cleanup with saved error + emitter.label(".Lutimensat_fail"); + emitter.instruction("call GetLastError"); // capture CreateFileW failure + emitter.instruction("mov DWORD PTR [rsp + 96], eax"); // preserve native error + emitter.label(".Lutimensat_fail_saved"); + emitter.instruction("mov rax, QWORD PTR [rsp + 88]"); // owned wide path + emitter.instruction("call __rt_heap_free"); // release path conversion + emitter.instruction("mov eax, DWORD PTR [rsp + 96]"); // restore native error + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // retain native state + emitter.instruction("call __rt_win32_errno_from_code"); // translate errno + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish errno + emitter.instruction("mov rax, -1"); // return -1 on failure + emitter.instruction("add rsp, 120"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lutimensat_conversion_fail"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 84"); // EILSEQ + emitter.instruction("mov rax, -1"); // return failure + emitter.instruction("add rsp, 120"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // fsync: delegate to FlushFileBuffers + emitter.label_global("fsync"); + emitter.instruction("sub rsp, 40"); // shadow space + emitter.instruction("mov rcx, rdi"); // fd + emitter.instruction("call __rt_fd_to_handle"); // convert fd to HANDLE + emitter.instruction("mov rcx, rax"); // handle + emitter.instruction("call FlushFileBuffers"); // flush file buffers to disk + emitter.instruction("test eax, eax"); // Win32 BOOL success? + emitter.instruction("jz .Lfsync_fail"); // translate a native flush failure + emitter.instruction("xor eax, eax"); // POSIX fsync success is zero + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return success + emitter.label(".Lfsync_fail"); + emitter.instruction("call GetLastError"); // capture the failed flush error + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // preserve native error state + emitter.instruction("call __rt_win32_errno_from_code"); // translate to POSIX errno + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish observable errno + emitter.instruction("mov eax, -1"); // POSIX fsync failure + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return failure + emitter.blank(); + + // __rt_sys_fdatasync: msvcrt has no `fdatasync` export. FlushFileBuffers + // (the `fsync` stub-delegate above) already flushes both data AND + // metadata, satisfying fdatasync's (weaker) contract — the same + // fallback `modify_x86_64.rs` already takes on Darwin (which also lacks + // `fdatasync`). Entered with fd in rdi (SysV — the emit_call_c call site + // is unchanged, only the symbol name routes here), so a bare tail-call + // preserves rdi for `fsync` unmodified; no frame of its own is needed. + emitter.label_global("__rt_sys_fdatasync"); + emitter.instruction("jmp fsync"); // tail-call: fsync's FlushFileBuffers already satisfies fdatasync + emitter.blank(); + + // chown/lchown/fchown: return -1 (ENOSYS) — Windows uses ACLs not Unix ownership + for sym in &["chown", "lchown", "fchown"] { + emitter.label_global(sym); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 38"); // ENOSYS: never report an unsupported ownership change as success + emitter.instruction("mov rax, -1"); // return -1 (not supported on Windows) + emitter.instruction("ret"); // return + emitter.blank(); + } + + // glob: real FindFirstFileExW/FindNextFileW enumeration. Builds a + // glibc-layout `glob_t` (gl_pathc @ offset 0, gl_pathv @ offset 8 — see + // `Platform::glob_pathv_offset`) into the caller's `glob_t*` (SysV arg4, + // rcx), which is exactly what `__rt_glob`/`opendir_glob.rs` already read + // on every other target. Win32 wildcard matching has no escape-character + // concept at all (backslash is only ever a path separator), so this is + // inherently GLOB_NOESCAPE — matching php-src's win32/glob.c — with no + // extra code needed. `.` and `..` are always filtered (POSIX/php-src + // glob() semantics; contrast with `opendir`/`readdir`, which DO return + // dot entries). Each matched leaf name from `WIN32_FIND_DATAW.cFileName` + // is converted back to UTF-8 and reassembled with the pattern's own + // directory prefix into a freshly `__rt_heap_alloc`'d C string. The + // `gl_pathv` array starts small and doubles as needed; growth preserves + // every owned path, checks the allocator's 32-bit payload-size contract, + // and cleans up the complete partial result on ENOMEM. `globfree` below + // frees every block this allocates. + emitter.label_global("glob"); + emitter.instruction("sub rsp, 760"); // shadow, WIN32_FIND_DATAW, and aligned glob locals + emitter.instruction("mov QWORD PTR [rsp + 656], rdi"); // save the pattern C-string pointer (SysV arg1) + emitter.instruction("mov QWORD PTR [rsp + 680], rcx"); // save the caller's glob_t* (SysV arg4) before it becomes MSx64 scratch + // -- compute dirpart length: index just past the LAST '/' or '\' in the pattern (0 = none) -- + emitter.instruction("xor r8, r8"); // scan index + emitter.instruction("xor r9, r9"); // dirpart length found so far + emitter.label(".Lglob_dirscan"); + emitter.instruction("movzx eax, BYTE PTR [rdi + r8]"); // load the next pattern byte + emitter.instruction("test al, al"); // stop at the terminating NUL + emitter.instruction("jz .Lglob_dirscan_done"); // dirpart length is now final + emitter.instruction("cmp al, 47"); // '/'? + emitter.instruction("je .Lglob_dirscan_sep"); // → record this separator + emitter.instruction("cmp al, 92"); // '\'? + emitter.instruction("jne .Lglob_dirscan_next"); // neither separator: keep scanning + emitter.label(".Lglob_dirscan_sep"); + emitter.instruction("lea r9, [r8 + 1]"); // dirpart length = index + 1 (keep the separator itself) + emitter.label(".Lglob_dirscan_next"); + emitter.instruction("inc r8"); // advance the scan index + emitter.instruction("jmp .Lglob_dirscan"); // continue scanning for the LAST separator + emitter.label(".Lglob_dirscan_done"); + emitter.instruction("mov QWORD PTR [rsp + 664], r9"); // save the dirpart length + emitter.instruction("mov QWORD PTR [rsp + 688], 0"); // initialize match count before any fallible allocation + emitter.instruction("mov QWORD PTR [rsp + 704], 16"); // initial gl_pathv capacity in pointer slots + emitter.instruction("mov QWORD PTR [rsp + 744], 0"); // no owned wide pattern yet + // -- allocate a small gl_pathv array and grow it geometrically on demand -- + emitter.instruction("mov rax, 128"); // initial 16 pointer slots * 8 bytes + emitter.instruction("call __rt_heap_alloc"); // rax = gl_pathv array storage + emitter.instruction("mov QWORD PTR [rsp + 696], rax"); // save the array pointer + emitter.instruction("test rax, rax"); // allocation succeeded? + emitter.instruction("jz .Lglob_nospace_no_handle"); // publish ENOMEM without entering enumeration + emitter.instruction("mov rdi, QWORD PTR [rsp + 656]"); // UTF-8 wildcard pattern + emitter.instruction("call __rt_win_utf8_to_utf16"); // allocate native Unicode wildcard pattern + emitter.instruction("mov QWORD PTR [rsp + 744], rax"); // preserve wide pattern through enumeration + emitter.instruction("test rax, rax"); // strict conversion succeeded? + emitter.instruction("jz .Lglob_nomatch"); // invalid UTF-8 has no Windows match + // -- FindFirstFileExW(pattern, FindExInfoBasic, &findData, NameMatch, NULL, LARGE_FETCH) -- + emitter.instruction("mov rcx, rax"); // lpFileName = wide pattern + emitter.instruction("mov edx, 1"); // FindExInfoBasic + emitter.instruction("lea r8, [rsp + 64]"); // lpFindFileData = &WIN32_FIND_DATAW + emitter.instruction("xor r9d, r9d"); // FindExSearchNameMatch + emitter.instruction("mov QWORD PTR [rsp + 32], 0"); // no search filter + emitter.instruction("mov QWORD PTR [rsp + 40], 2"); // FIND_FIRST_EX_LARGE_FETCH + emitter.instruction("call FindFirstFileExW"); // find the first Unicode matching entry + emitter.instruction("mov QWORD PTR [rsp + 672], rax"); // save hFind + emitter.instruction("cmp rax, -1"); // INVALID_HANDLE_VALUE? + emitter.instruction("je .Lglob_nomatch"); // no match at all: report GLOB_NOMATCH + emitter.label(".Lglob_loop"); + // -- skip "." and ".." (glob() never returns them, unlike opendir/readdir) -- + emitter.instruction("lea rax, [rsp + 64 + 44]"); // wide cFileName + emitter.instruction("movzx ecx, WORD PTR [rax]"); // cFileName[0] + emitter.instruction("cmp cx, 46"); // '.'? + emitter.instruction("jne .Lglob_not_dot"); // not a dot entry + emitter.instruction("movzx ecx, WORD PTR [rax + 2]"); // cFileName[1] + emitter.instruction("test cx, cx"); // NUL right after the first '.'? + emitter.instruction("jz .Lglob_skip_entry"); // "." — drop it + emitter.instruction("cmp cx, 46"); // second '.'? + emitter.instruction("jne .Lglob_not_dot"); // some other ".x" name: keep it + emitter.instruction("movzx ecx, WORD PTR [rax + 4]"); // cFileName[2] + emitter.instruction("test cx, cx"); // NUL right after ".."? + emitter.instruction("jz .Lglob_skip_entry"); // ".." — drop it + emitter.label(".Lglob_not_dot"); + // -- double gl_pathv when count reaches capacity, preserving owned paths -- + emitter.instruction("mov rax, QWORD PTR [rsp + 688]"); // reload the current match count + emitter.instruction("cmp rax, QWORD PTR [rsp + 704]"); // has the array reached its current capacity? + emitter.instruction("jb .Lglob_capacity_ready"); // there is already room for this match + emitter.instruction("mov rcx, QWORD PTR [rsp + 704]"); // old capacity in pointer slots + emitter.instruction("cmp rcx, 0x10000000"); // would doubling exceed the heap allocator's 32-bit payload size? + emitter.instruction("jae .Lglob_nospace_open"); // reject arithmetic overflow as ENOMEM + emitter.instruction("lea rax, [rcx + rcx]"); // new capacity = old capacity * 2 + emitter.instruction("mov QWORD PTR [rsp + 704], rax"); // retain the new capacity across allocation + emitter.instruction("shl rax, 3"); // convert pointer slots to allocation bytes + emitter.instruction("call __rt_heap_alloc"); // allocate the larger pointer array + emitter.instruction("test rax, rax"); // allocation succeeded? + emitter.instruction("jz .Lglob_nospace_open"); // keep the old array intact for complete cleanup + emitter.instruction("mov QWORD PTR [rsp + 752], rax"); // preserve the new array while copying and freeing + emitter.instruction("xor rcx, rcx"); // copy index = 0 + emitter.label(".Lglob_grow_copy"); + emitter.instruction("cmp rcx, QWORD PTR [rsp + 688]"); // copied every populated pointer? + emitter.instruction("jae .Lglob_grow_copy_done"); // switch ownership to the new array + emitter.instruction("mov r10, QWORD PTR [rsp + 696]"); // old array base + emitter.instruction("mov r11, QWORD PTR [r10 + rcx * 8]"); // load one owned path pointer + emitter.instruction("mov r10, QWORD PTR [rsp + 752]"); // new array base + emitter.instruction("mov QWORD PTR [r10 + rcx * 8], r11"); // preserve the path in the corresponding slot + emitter.instruction("inc rcx"); // advance to the next populated pointer + emitter.instruction("jmp .Lglob_grow_copy"); // continue copying the partial result + emitter.label(".Lglob_grow_copy_done"); + emitter.instruction("mov rax, QWORD PTR [rsp + 696]"); // old gl_pathv allocation + emitter.instruction("call __rt_heap_free"); // release obsolete pointer storage only + emitter.instruction("mov rax, QWORD PTR [rsp + 752]"); // reload the replacement array + emitter.instruction("mov QWORD PTR [rsp + 696], rax"); // publish the grown gl_pathv base + emitter.label(".Lglob_capacity_ready"); + // -- allocate prefix plus the worst-case UTF-8 expansion of MAX_PATH WCHARs -- + emitter.instruction("mov rax, QWORD PTR [rsp + 664]"); // dirpart length + emitter.instruction("add rax, 1040"); // four UTF-8 bytes per WCHAR including terminator capacity + emitter.instruction("jc .Lglob_nospace_open"); // reject prefix-size arithmetic overflow as ENOMEM + emitter.instruction("mov r10, rax"); // preserve the request while checking its high 32 bits + emitter.instruction("shr r10, 32"); // isolate bytes outside the allocator's payload-size field + emitter.instruction("jnz .Lglob_nospace_open"); // reject size truncation as ENOMEM + emitter.instruction("call __rt_heap_alloc"); // rax = new path buffer + emitter.instruction("mov QWORD PTR [rsp + 712], rax"); // save the buffer pointer + emitter.instruction("test rax, rax"); // path allocation succeeded? + emitter.instruction("jz .Lglob_nospace_open"); // clean the complete partial result on ENOMEM + // -- copy the dirpart prefix from the original pattern -- + emitter.instruction("mov r10, QWORD PTR [rsp + 712]"); // dest buffer + emitter.instruction("mov r11, QWORD PTR [rsp + 656]"); // src = the original pattern + emitter.instruction("xor r9, r9"); // copy index + emitter.instruction("mov r8, QWORD PTR [rsp + 664]"); // dirpart length + emitter.label(".Lglob_copy_dir"); + emitter.instruction("cmp r9, r8"); // copied the whole dirpart prefix? + emitter.instruction("jae .Lglob_copy_dir_done"); // → append the leaf name next + emitter.instruction("movzx eax, BYTE PTR [r11 + r9]"); // load the next dirpart byte + emitter.instruction("mov BYTE PTR [r10 + r9], al"); // store it into the destination buffer + emitter.instruction("inc r9"); // advance the copy index + emitter.instruction("jmp .Lglob_copy_dir"); // continue copying the dirpart prefix + emitter.label(".Lglob_copy_dir_done"); + // -- append the matched wide leaf name as strict UTF-8 -- + emitter.instruction("lea rsi, [r10 + r8]"); // destination cursor after the UTF-8 prefix + emitter.instruction("lea rdi, [rsp + 64 + 44]"); // source WIN32_FIND_DATAW.cFileName + emitter.instruction("mov rdx, 1040"); // destination capacity + emitter.instruction("call __rt_win_utf16_to_utf8"); // convert the native leaf name + emitter.instruction("test eax, eax"); // conversion succeeded? + emitter.instruction("jz .Lglob_conversion_fail"); // drop unrepresentable native entries safely + // -- store the new path pointer into gl_pathv[count] and advance count -- + emitter.instruction("mov rax, QWORD PTR [rsp + 688]"); // reload the current match count + emitter.instruction("mov r10, QWORD PTR [rsp + 696]"); // gl_pathv array base + emitter.instruction("mov r11, QWORD PTR [rsp + 712]"); // the reassembled full-path buffer + emitter.instruction("mov QWORD PTR [r10 + rax * 8], r11"); // gl_pathv[count] = full path + emitter.instruction("inc rax"); // count++ + emitter.instruction("mov QWORD PTR [rsp + 688], rax"); // save the updated match count + emitter.instruction("jmp .Lglob_skip_entry"); // continue enumeration + emitter.label(".Lglob_conversion_fail"); + emitter.instruction("mov rax, QWORD PTR [rsp + 712]"); // path allocation for the failed conversion + emitter.instruction("call __rt_heap_free"); // avoid leaking a dropped entry + emitter.label(".Lglob_skip_entry"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 672]"); // hFind + emitter.instruction("lea rdx, [rsp + 64]"); // &WIN32_FIND_DATAW reused for the next entry + emitter.instruction("call FindNextFileW"); // advance to the next Unicode directory entry + emitter.instruction("test eax, eax"); // more entries? + emitter.instruction("jnz .Lglob_loop"); // continue the enumeration loop + emitter.instruction("mov rcx, QWORD PTR [rsp + 672]"); // hFind + emitter.instruction("call FindClose"); // release the Win32 search handle + emitter.instruction("mov rax, QWORD PTR [rsp + 744]"); // retained UTF-16 wildcard pattern + emitter.instruction("call __rt_heap_free"); // release native pattern after enumeration + // -- sort gl_pathv[0..count) lexicographically (selection sort): Win32 + // directory enumeration order is not guaranteed, but every other + // target's libc glob() returns sorted matches and callers (e.g. the + // `glob://` stream wrapper's ordered readdir() iteration) rely on it — + // matches php-src's own win32/glob.c, which also explicitly sorts. -- + emitter.instruction("mov QWORD PTR [rsp + 720], 0"); // i = 0 + emitter.label(".Lglob_sort_outer"); + emitter.instruction("mov rax, QWORD PTR [rsp + 720]"); // i + emitter.instruction("mov rcx, QWORD PTR [rsp + 688]"); // count + emitter.instruction("cmp rcx, 1"); // zero or one result is already sorted + emitter.instruction("jbe .Lglob_sort_done"); // avoid count-1 underflow for an empty filtered result + emitter.instruction("dec rcx"); // count - 1 + emitter.instruction("cmp rax, rcx"); // i < count - 1? + emitter.instruction("jae .Lglob_sort_done"); // fewer than 2 remaining: sorted + emitter.instruction("mov QWORD PTR [rsp + 728], rax"); // min_idx = i + emitter.instruction("lea rax, [rax + 1]"); // j = i + 1 + emitter.instruction("mov QWORD PTR [rsp + 736], rax"); // save j + emitter.label(".Lglob_sort_inner"); + emitter.instruction("mov rax, QWORD PTR [rsp + 736]"); // j + emitter.instruction("cmp rax, QWORD PTR [rsp + 688]"); // j < count? + emitter.instruction("jae .Lglob_sort_inner_done"); // inner scan finished + emitter.instruction("mov r10, QWORD PTR [rsp + 696]"); // gl_pathv array base + emitter.instruction("mov r11, QWORD PTR [r10 + rax * 8]"); // pathv[j] + emitter.instruction("mov rcx, QWORD PTR [rsp + 728]"); // min_idx + emitter.instruction("mov r9, QWORD PTR [r10 + rcx * 8]"); // pathv[min_idx] + emitter.instruction("xor rcx, rcx"); // strcmp byte index + emitter.label(".Lglob_strcmp_loop"); + emitter.instruction("movzx eax, BYTE PTR [r11 + rcx]"); // pathv[j][idx] + emitter.instruction("movzx edx, BYTE PTR [r9 + rcx]"); // pathv[min_idx][idx] + emitter.instruction("cmp al, dl"); // compare this byte pair + emitter.instruction("jl .Lglob_strcmp_less"); // pathv[j] sorts before pathv[min_idx] + emitter.instruction("jg .Lglob_strcmp_next_j"); // pathv[j] sorts after: min_idx stays + emitter.instruction("test al, al"); // equal bytes — both strings ended (NUL)? + emitter.instruction("jz .Lglob_strcmp_next_j"); // identical strings: min_idx stays (stable) + emitter.instruction("inc rcx"); // advance to the next byte pair + emitter.instruction("jmp .Lglob_strcmp_loop"); // keep comparing + emitter.label(".Lglob_strcmp_less"); + emitter.instruction("mov rax, QWORD PTR [rsp + 736]"); // j + emitter.instruction("mov QWORD PTR [rsp + 728], rax"); // min_idx = j + emitter.label(".Lglob_strcmp_next_j"); + emitter.instruction("mov rax, QWORD PTR [rsp + 736]"); // reload j + emitter.instruction("inc rax"); // j++ + emitter.instruction("mov QWORD PTR [rsp + 736], rax"); // save the advanced j + emitter.instruction("jmp .Lglob_sort_inner"); // continue the inner scan + emitter.label(".Lglob_sort_inner_done"); + emitter.instruction("mov rax, QWORD PTR [rsp + 720]"); // i + emitter.instruction("mov rcx, QWORD PTR [rsp + 728]"); // min_idx + emitter.instruction("cmp rax, rcx"); // already in place? + emitter.instruction("je .Lglob_sort_no_swap"); // nothing to swap + emitter.instruction("mov r10, QWORD PTR [rsp + 696]"); // gl_pathv array base + emitter.instruction("mov r11, QWORD PTR [r10 + rax * 8]"); // pathv[i] + emitter.instruction("mov r9, QWORD PTR [r10 + rcx * 8]"); // pathv[min_idx] + emitter.instruction("mov QWORD PTR [r10 + rax * 8], r9"); // pathv[i] = old pathv[min_idx] + emitter.instruction("mov QWORD PTR [r10 + rcx * 8], r11"); // pathv[min_idx] = old pathv[i] + emitter.label(".Lglob_sort_no_swap"); + emitter.instruction("mov rax, QWORD PTR [rsp + 720]"); // reload i + emitter.instruction("inc rax"); // i++ + emitter.instruction("mov QWORD PTR [rsp + 720], rax"); // save the advanced i + emitter.instruction("jmp .Lglob_sort_outer"); // select the next minimum + emitter.label(".Lglob_sort_done"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 680]"); // the caller's glob_t* + emitter.instruction("mov rax, QWORD PTR [rsp + 688]"); // final match count + emitter.instruction("mov QWORD PTR [rcx], rax"); // gl_pathc = match count + emitter.instruction("mov rax, QWORD PTR [rsp + 696]"); // gl_pathv array pointer + emitter.instruction("mov QWORD PTR [rcx + 8], rax"); // gl_pathv = array pointer + emitter.instruction("xor rax, rax"); // return 0 (success) + emitter.instruction("add rsp, 760"); // restore stack + emitter.instruction("ret"); // return + // -- allocation/overflow failure: close enumeration and free the partial result -- + emitter.label(".Lglob_nospace_open"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 672]"); // active Win32 enumeration handle + emitter.instruction("call FindClose"); // release the search handle before heap cleanup + emitter.instruction("mov rax, QWORD PTR [rsp + 744]"); // owned UTF-16 wildcard pattern + emitter.instruction("test rax, rax"); // was the conversion allocated? + emitter.instruction("jz .Lglob_nospace_paths"); // skip a null pattern allocation + emitter.instruction("call __rt_heap_free"); // release the native wildcard pattern + emitter.label(".Lglob_nospace_paths"); + emitter.instruction("mov QWORD PTR [rsp + 720], 0"); // cleanup index = 0 + emitter.label(".Lglob_nospace_path_loop"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 720]"); // reload cleanup index + emitter.instruction("cmp rcx, QWORD PTR [rsp + 688]"); // released every completed path? + emitter.instruction("jae .Lglob_nospace_array"); // continue with pointer-array cleanup + emitter.instruction("mov r10, QWORD PTR [rsp + 696]"); // current gl_pathv array base + emitter.instruction("mov rax, QWORD PTR [r10 + rcx * 8]"); // one completed owned path + emitter.instruction("call __rt_heap_free"); // release the partial result path + emitter.instruction("mov rcx, QWORD PTR [rsp + 720]"); // restore cleanup index after the call + emitter.instruction("inc rcx"); // advance to the next completed path + emitter.instruction("mov QWORD PTR [rsp + 720], rcx"); // persist the cleanup index + emitter.instruction("jmp .Lglob_nospace_path_loop"); // release the rest of the partial result + emitter.label(".Lglob_nospace_array"); + emitter.instruction("mov rax, QWORD PTR [rsp + 696]"); // current gl_pathv allocation + emitter.instruction("test rax, rax"); // was pointer storage allocated? + emitter.instruction("jz .Lglob_nospace_no_handle"); // initial allocation failure has nothing to free + emitter.instruction("call __rt_heap_free"); // release pointer-array storage + emitter.label(".Lglob_nospace_no_handle"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 680]"); // caller's glob_t output + emitter.instruction("mov QWORD PTR [rcx], 0"); // leave gl_pathc empty on failure + emitter.instruction("mov QWORD PTR [rcx + 8], 0"); // leave gl_pathv null on failure + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 12"); // ENOMEM for allocation or representable-size exhaustion + emitter.instruction("mov rax, 1"); // return GLOB_NOSPACE + emitter.instruction("add rsp, 760"); // restore stack + emitter.instruction("ret"); // return failure after complete cleanup + emitter.label(".Lglob_nomatch"); + emitter.instruction("mov rax, QWORD PTR [rsp + 744]"); // converted wildcard pattern, possibly NULL + emitter.instruction("test rax, rax"); // was conversion allocated? + emitter.instruction("jz .Lglob_nomatch_free_pathv"); // skip NULL cleanup + emitter.instruction("call __rt_heap_free"); // release wide pattern after failed enumeration + emitter.label(".Lglob_nomatch_free_pathv"); + emitter.instruction("mov rax, QWORD PTR [rsp + 696]"); // the unused gl_pathv array allocation + emitter.instruction("call __rt_heap_free"); // release it before reporting failure + emitter.instruction("mov rax, 1"); // return GLOB_NOMATCH (any nonzero — callers only test != 0) + emitter.instruction("add rsp, 760"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // globfree: releases every block `glob` (above) allocated — each + // matched path string plus the gl_pathv array itself. SysV: rdi = &glob_t + // (gl_pathc @ offset 0, gl_pathv @ offset 8), matching every call site + // (`__rt_glob`, `opendir_glob.rs`, `closedir.rs`). + emitter.label_global("globfree"); + emitter.instruction("sub rsp, 40"); // locals: count(8) + array ptr(8) + index(8) + pad(16), 16B aligned + emitter.instruction("mov rax, QWORD PTR [rdi]"); // gl_pathc + emitter.instruction("mov QWORD PTR [rsp], rax"); // save the match count + emitter.instruction("mov rax, QWORD PTR [rdi + 8]"); // gl_pathv + emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // save the array pointer + emitter.instruction("test rax, rax"); // was an array ever allocated? + emitter.instruction("jz .Lglobfree_done"); // nothing to free (a never-populated glob_t) + emitter.instruction("mov QWORD PTR [rsp + 16], 0"); // index = 0 + emitter.label(".Lglobfree_loop"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 16]"); // reload the current index + emitter.instruction("cmp rcx, QWORD PTR [rsp]"); // freed every matched path string? + emitter.instruction("jae .Lglobfree_free_array"); // → free the array storage itself + emitter.instruction("mov r10, QWORD PTR [rsp + 8]"); // array base + emitter.instruction("mov rax, QWORD PTR [r10 + rcx * 8]"); // array[index] = one matched path string + emitter.instruction("call __rt_heap_free"); // release that path string + emitter.instruction("mov rcx, QWORD PTR [rsp + 16]"); // reload the index (clobbered by the call) + emitter.instruction("inc rcx"); // advance to the next slot + emitter.instruction("mov QWORD PTR [rsp + 16], rcx"); // save the updated index + emitter.instruction("jmp .Lglobfree_loop"); // continue freeing matched path strings + emitter.label(".Lglobfree_free_array"); + emitter.instruction("mov rax, QWORD PTR [rsp + 8]"); // the gl_pathv array itself + emitter.instruction("call __rt_heap_free"); // release the array storage + emitter.label(".Lglobfree_done"); + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // fnmatch: PHP/POSIX byte matcher. PathMatchSpecW is deliberately not used: + // its shell wildcard grammar ignores FNM_PATHNAME/FNM_PERIOD/FNM_NOESCAPE + // and is always case-insensitive, unlike php-src's Windows fnmatch port. + emitter.label_global("fnmatch"); + emitter.instruction("sub rsp, 40"); // reserve flags, star-backtrack cursors, and original string + emitter.instruction("mov QWORD PTR [rsp], rdx"); // preserve FNM_* flags + emitter.instruction("mov QWORD PTR [rsp + 8], 0"); // no remembered pattern position after a star + emitter.instruction("mov QWORD PTR [rsp + 16], 0"); // no remembered candidate position for a star + emitter.instruction("mov QWORD PTR [rsp + 24], rsi"); // preserve candidate start for FNM_PERIOD + emitter.label(".Lfnmatch_loop"); + emitter.instruction("movzx eax, BYTE PTR [rdi]"); // load the next pattern byte + emitter.instruction("test al, al"); // end of pattern? + emitter.instruction("jz .Lfnmatch_pattern_end"); // only a simultaneous candidate end matches + emitter.instruction("cmp al, 42"); // '*' wildcard? + emitter.instruction("je .Lfnmatch_star"); // remember a bounded backtracking point + emitter.instruction("cmp al, 63"); // '?' wildcard? + emitter.instruction("je .Lfnmatch_question"); // match exactly one permitted byte + emitter.instruction("cmp al, 91"); // '[' character class? + emitter.instruction("je .Lfnmatch_class"); // parse and match the bracket expression + emitter.instruction("cmp al, 92"); // backslash escape? + emitter.instruction("jne .Lfnmatch_literal"); // ordinary byte comparison + emitter.instruction("test QWORD PTR [rsp], 2"); // FNM_NOESCAPE leaves backslash literal + emitter.instruction("jnz .Lfnmatch_literal"); // do not consume the escaped byte + emitter.instruction("cmp BYTE PTR [rdi + 1], 0"); // dangling backslash? + emitter.instruction("je .Lfnmatch_literal"); // treat a terminal backslash literally + emitter.instruction("inc rdi"); // skip the escape marker + emitter.instruction("movzx eax, BYTE PTR [rdi]"); // compare the escaped byte literally + emitter.label(".Lfnmatch_literal"); + emitter.instruction("movzx ecx, BYTE PTR [rsi]"); // load the candidate byte + emitter.instruction("test cl, cl"); // candidate exhausted? + emitter.instruction("jz .Lfnmatch_backtrack"); // a literal cannot match the terminator + emitter.instruction("mov r8d, eax"); // preserve the pattern byte for optional folding + emitter.instruction("mov r9d, ecx"); // preserve the candidate byte for optional folding + emitter.instruction("test QWORD PTR [rsp], 16"); // FNM_CASEFOLD enabled? + emitter.instruction("jz .Lfnmatch_literal_compare"); // compare bytes exactly otherwise + emitter.instruction("cmp r8b, 65"); // pattern byte below ASCII 'A'? + emitter.instruction("jb .Lfnmatch_fold_candidate"); // skip pattern folding + emitter.instruction("cmp r8b, 90"); // pattern byte above ASCII 'Z'? + emitter.instruction("ja .Lfnmatch_fold_candidate"); // skip pattern folding + emitter.instruction("or r8b, 32"); // fold pattern ASCII uppercase to lowercase + emitter.label(".Lfnmatch_fold_candidate"); + emitter.instruction("cmp r9b, 65"); // candidate byte below ASCII 'A'? + emitter.instruction("jb .Lfnmatch_literal_compare"); // skip candidate folding + emitter.instruction("cmp r9b, 90"); // candidate byte above ASCII 'Z'? + emitter.instruction("ja .Lfnmatch_literal_compare"); // skip candidate folding + emitter.instruction("or r9b, 32"); // fold candidate ASCII uppercase to lowercase + emitter.label(".Lfnmatch_literal_compare"); + emitter.instruction("cmp r8b, r9b"); // do the literal bytes match? + emitter.instruction("jne .Lfnmatch_backtrack"); // retry through the most recent star on mismatch + emitter.instruction("inc rdi"); // consume one pattern byte + emitter.instruction("inc rsi"); // consume one candidate byte + emitter.instruction("jmp .Lfnmatch_loop"); // continue matching + emitter.label(".Lfnmatch_question"); + emitter.instruction("movzx ecx, BYTE PTR [rsi]"); // load the wildcard candidate byte + emitter.instruction("test cl, cl"); // '?' cannot match the terminator + emitter.instruction("jz .Lfnmatch_backtrack"); // retry through a preceding star + emitter.instruction("test QWORD PTR [rsp], 1"); // FNM_PATHNAME enabled? + emitter.instruction("jz .Lfnmatch_question_period"); // slash is ordinary without the flag + emitter.instruction("cmp cl, 47"); // candidate is '/'? + emitter.instruction("je .Lfnmatch_backtrack"); // wildcard cannot cross a path separator + emitter.label(".Lfnmatch_question_period"); + emitter.instruction("cmp cl, 46"); // candidate is '.'? + emitter.instruction("jne .Lfnmatch_question_consume"); // no leading-period restriction + emitter.instruction("call .Lfnmatch_is_leading_period"); // rax = whether wildcard sees a leading period + emitter.instruction("test eax, eax"); // is FNM_PERIOD blocking this wildcard? + emitter.instruction("jnz .Lfnmatch_backtrack"); // leading period must be matched literally + emitter.label(".Lfnmatch_question_consume"); + emitter.instruction("inc rdi"); // consume '?' + emitter.instruction("inc rsi"); // consume its candidate byte + emitter.instruction("jmp .Lfnmatch_loop"); // continue matching + emitter.label(".Lfnmatch_star"); + emitter.instruction("inc rdi"); // consume the first '*' + emitter.label(".Lfnmatch_star_collapse"); + emitter.instruction("cmp BYTE PTR [rdi], 42"); // adjacent '*' wildcard? + emitter.instruction("jne .Lfnmatch_star_ready"); // keep one canonical star + emitter.instruction("inc rdi"); // collapse the redundant wildcard + emitter.instruction("jmp .Lfnmatch_star_collapse"); // continue collapsing + emitter.label(".Lfnmatch_star_ready"); + emitter.instruction("mov QWORD PTR [rsp + 8], rdi"); // retry pattern immediately after the star + emitter.instruction("mov QWORD PTR [rsp + 16], rsi"); // initially let '*' consume zero bytes + emitter.instruction("jmp .Lfnmatch_loop"); // attempt the zero-length match first + emitter.label(".Lfnmatch_class"); + emitter.instruction("movzx ecx, BYTE PTR [rsi]"); // load the class candidate byte + emitter.instruction("test cl, cl"); // a class cannot match the terminator + emitter.instruction("jz .Lfnmatch_backtrack"); // retry through a preceding star + emitter.instruction("test QWORD PTR [rsp], 1"); // FNM_PATHNAME enabled? + emitter.instruction("jz .Lfnmatch_class_period"); // slash is class-matchable without the flag + emitter.instruction("cmp cl, 47"); // candidate is '/'? + emitter.instruction("je .Lfnmatch_backtrack"); // classes cannot cross a path separator + emitter.label(".Lfnmatch_class_period"); + emitter.instruction("cmp cl, 46"); // candidate is '.'? + emitter.instruction("jne .Lfnmatch_class_parse"); // no leading-period restriction + emitter.instruction("call .Lfnmatch_is_leading_period"); // rax = whether wildcard sees a leading period + emitter.instruction("test eax, eax"); // is FNM_PERIOD blocking this class? + emitter.instruction("jnz .Lfnmatch_backtrack"); // leading period must be literal + emitter.label(".Lfnmatch_class_parse"); + emitter.instruction("lea r8, [rdi + 1]"); // class cursor after '[' + emitter.instruction("xor r9d, r9d"); // class is positive by default + emitter.instruction("cmp BYTE PTR [r8], 33"); // leading '!' negates the class + emitter.instruction("je .Lfnmatch_class_negate"); // record class negation + emitter.instruction("cmp BYTE PTR [r8], 94"); // accept '^' as the alternate negator + emitter.instruction("jne .Lfnmatch_class_begin"); // begin scanning members + emitter.label(".Lfnmatch_class_negate"); + emitter.instruction("mov r9d, 1"); // remember negated class semantics + emitter.instruction("inc r8"); // skip the negation marker + emitter.label(".Lfnmatch_class_begin"); + emitter.instruction("xor r10d, r10d"); // no class member has matched yet + emitter.label(".Lfnmatch_class_scan"); + emitter.instruction("movzx eax, BYTE PTR [r8]"); // load the next class byte + emitter.instruction("test al, al"); // unterminated class? + emitter.instruction("jz .Lfnmatch_class_invalid"); // treat '[' as a literal + emitter.instruction("cmp al, 93"); // closing ']'? + emitter.instruction("je .Lfnmatch_class_done"); // finish the class decision + emitter.instruction("mov r11d, eax"); // lower endpoint defaults to this member + emitter.instruction("cmp BYTE PTR [r8 + 1], 45"); // range marker follows? + emitter.instruction("jne .Lfnmatch_class_single"); // compare one member + emitter.instruction("cmp BYTE PTR [r8 + 2], 0"); // missing range upper endpoint? + emitter.instruction("je .Lfnmatch_class_single"); // '-' is literal at the end + emitter.instruction("cmp BYTE PTR [r8 + 2], 93"); // '-' immediately before closing bracket? + emitter.instruction("je .Lfnmatch_class_single"); // keep '-' literal + emitter.instruction("movzx eax, BYTE PTR [r8 + 2]"); // load the range upper endpoint + emitter.instruction("mov rdx, QWORD PTR [rsp]"); // reload flags for the shared comparison + emitter.instruction("jmp .Lfnmatch_class_compare_range"); // test candidate within the range + emitter.label(".Lfnmatch_class_single"); + emitter.instruction("mov eax, r11d"); // single member has equal lower/upper endpoints + emitter.instruction("mov rdx, QWORD PTR [rsp]"); // reload flags for the shared comparison + emitter.label(".Lfnmatch_class_compare_range"); + emitter.instruction("movzx ecx, BYTE PTR [rsi]"); // reload candidate byte + emitter.instruction("test rdx, 16"); // FNM_CASEFOLD enabled? + emitter.instruction("jz .Lfnmatch_class_compare"); // retain exact byte order otherwise + emitter.instruction("cmp r11b, 65"); // lower endpoint below ASCII uppercase? + emitter.instruction("jb .Lfnmatch_class_fold_upper"); // skip lower folding + emitter.instruction("cmp r11b, 90"); // lower endpoint above ASCII uppercase? + emitter.instruction("ja .Lfnmatch_class_fold_upper"); // skip lower folding + emitter.instruction("or r11b, 32"); // fold lower endpoint + emitter.label(".Lfnmatch_class_fold_upper"); + emitter.instruction("cmp al, 65"); // upper endpoint below ASCII uppercase? + emitter.instruction("jb .Lfnmatch_class_fold_candidate"); // skip upper folding + emitter.instruction("cmp al, 90"); // upper endpoint above ASCII uppercase? + emitter.instruction("ja .Lfnmatch_class_fold_candidate"); // skip upper folding + emitter.instruction("or al, 32"); // fold upper endpoint + emitter.label(".Lfnmatch_class_fold_candidate"); + emitter.instruction("cmp cl, 65"); // candidate below ASCII uppercase? + emitter.instruction("jb .Lfnmatch_class_compare"); // skip candidate folding + emitter.instruction("cmp cl, 90"); // candidate above ASCII uppercase? + emitter.instruction("ja .Lfnmatch_class_compare"); // skip candidate folding + emitter.instruction("or cl, 32"); // fold candidate + emitter.label(".Lfnmatch_class_compare"); + emitter.instruction("cmp cl, r11b"); // candidate below lower endpoint? + emitter.instruction("jb .Lfnmatch_class_advance"); // this member does not match + emitter.instruction("cmp cl, al"); // candidate above upper endpoint? + emitter.instruction("ja .Lfnmatch_class_advance"); // this member does not match + emitter.instruction("mov r10d, 1"); // remember that one member matched + emitter.label(".Lfnmatch_class_advance"); + emitter.instruction("cmp BYTE PTR [r8 + 1], 45"); // was this syntactically a range? + emitter.instruction("jne .Lfnmatch_class_advance_one"); // consume one member + emitter.instruction("cmp BYTE PTR [r8 + 2], 0"); // malformed range endpoint? + emitter.instruction("je .Lfnmatch_class_advance_one"); // consume one member + emitter.instruction("cmp BYTE PTR [r8 + 2], 93"); // '-' was literal before closing bracket? + emitter.instruction("je .Lfnmatch_class_advance_one"); // consume one member + emitter.instruction("add r8, 3"); // consume the complete range + emitter.instruction("jmp .Lfnmatch_class_scan"); // scan remaining members + emitter.label(".Lfnmatch_class_advance_one"); + emitter.instruction("inc r8"); // consume one class member + emitter.instruction("jmp .Lfnmatch_class_scan"); // scan remaining members + emitter.label(".Lfnmatch_class_done"); + emitter.instruction("cmp r10d, r9d"); // positive wants matched=1; negated wants matched=0 + emitter.instruction("je .Lfnmatch_backtrack"); // class decision rejected the candidate + emitter.instruction("lea rdi, [r8 + 1]"); // consume the whole bracket expression + emitter.instruction("inc rsi"); // consume its candidate byte + emitter.instruction("mov rdx, QWORD PTR [rsp]"); // restore flags after class scratch use + emitter.instruction("jmp .Lfnmatch_loop"); // continue matching + emitter.label(".Lfnmatch_class_invalid"); + emitter.instruction("mov rdx, QWORD PTR [rsp]"); // restore flags after class scratch use + emitter.instruction("mov eax, 91"); // malformed '[' is matched literally + emitter.instruction("jmp .Lfnmatch_literal"); // use ordinary literal comparison + emitter.label(".Lfnmatch_pattern_end"); + emitter.instruction("cmp BYTE PTR [rsi], 0"); // candidate also exhausted? + emitter.instruction("jne .Lfnmatch_backtrack"); // let a preceding star absorb more bytes + emitter.instruction("xor eax, eax"); // libc fnmatch success is zero + emitter.instruction("add rsp, 40"); // release matcher locals + emitter.instruction("ret"); // return match + emitter.label(".Lfnmatch_backtrack"); + emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // reload the pattern position after the latest star + emitter.instruction("test rdi, rdi"); // was any star seen? + emitter.instruction("jz .Lfnmatch_nomatch"); // no remaining matching alternative + emitter.instruction("mov rsi, QWORD PTR [rsp + 16]"); // reload how far that star currently consumes + emitter.instruction("movzx ecx, BYTE PTR [rsi]"); // inspect the next byte the star might absorb + emitter.instruction("test cl, cl"); // candidate exhausted? + emitter.instruction("jz .Lfnmatch_nomatch"); // star has no larger alternative + emitter.instruction("test QWORD PTR [rsp], 1"); // FNM_PATHNAME enabled? + emitter.instruction("jz .Lfnmatch_backtrack_period"); // slash may be absorbed otherwise + emitter.instruction("cmp cl, 47"); // next byte is '/'? + emitter.instruction("je .Lfnmatch_nomatch"); // star cannot cross a separator + emitter.label(".Lfnmatch_backtrack_period"); + emitter.instruction("cmp cl, 46"); // next byte is '.'? + emitter.instruction("jne .Lfnmatch_backtrack_consume"); // no leading-period restriction + emitter.instruction("call .Lfnmatch_is_leading_period"); // rax = whether wildcard sees a leading period + emitter.instruction("test eax, eax"); // is FNM_PERIOD blocking the star? + emitter.instruction("jnz .Lfnmatch_nomatch"); // star cannot consume that leading period + emitter.label(".Lfnmatch_backtrack_consume"); + emitter.instruction("inc rsi"); // let the star consume one additional byte + emitter.instruction("mov QWORD PTR [rsp + 16], rsi"); // remember the expanded candidate position + emitter.instruction("jmp .Lfnmatch_loop"); // retry the suffix + emitter.label(".Lfnmatch_is_leading_period"); + emitter.instruction("xor eax, eax"); // default: wildcard may consume this period + emitter.instruction("test QWORD PTR [rsp + 8], 4"); // FNM_PERIOD enabled? (return address precedes matcher locals) + emitter.instruction("jz .Lfnmatch_period_return"); // no restriction without the flag + emitter.instruction("cmp rsi, QWORD PTR [rsp + 32]"); // beginning of the whole candidate? (account for return address) + emitter.instruction("je .Lfnmatch_period_yes"); // leading period is protected + emitter.instruction("test QWORD PTR [rsp + 8], 1"); // only pathname mode starts segments after '/' (account for return address) + emitter.instruction("jz .Lfnmatch_period_return"); // interior periods are ordinary + emitter.instruction("cmp BYTE PTR [rsi - 1], 47"); // immediately follows a separator? + emitter.instruction("jne .Lfnmatch_period_return"); // not a segment-leading period + emitter.label(".Lfnmatch_period_yes"); + emitter.instruction("mov eax, 1"); // report a protected leading period + emitter.label(".Lfnmatch_period_return"); + emitter.instruction("ret"); // return to wildcard decision + emitter.label(".Lfnmatch_nomatch"); + emitter.instruction("mov rax, 1"); // return FNM_NOMATCH (1) + emitter.instruction("add rsp, 40"); // release matcher locals + emitter.instruction("ret"); // return + emitter.blank(); + + // realpath: resolve the opened object through GetFinalPathNameByHandleW and return strict UTF-8. + emitter.label_global("realpath"); + emitter.instruction("sub rsp, 104"); // shadow space, outgoing Win32 arguments, and owned input/output paths + emitter.instruction("mov QWORD PTR [rsp + 96], rsi"); // preserve caller UTF-8 destination outside outgoing argument slots + emitter.instruction("call __rt_win_utf8_to_utf16"); // convert input path strictly + emitter.instruction("test rax, rax"); // conversion succeeded? + emitter.instruction("jz .Lrealpath_conversion_fail"); // invalid UTF-8 + emitter.instruction("mov QWORD PTR [rsp + 88], rax"); // retain owned wide input outside outgoing argument slots + emitter.instruction("mov rax, 65536"); // maximum extended wide path buffer + emitter.instruction("call __rt_heap_alloc"); // allocate UTF-16 resolved path + emitter.instruction("test rax, rax"); // allocation succeeded? + emitter.instruction("jz .Lrealpath_alloc_fail"); // cleanup input and return ENOMEM + emitter.instruction("mov QWORD PTR [rsp + 80], rax"); // retain owned wide result outside Win32 outgoing argument slots + emitter.instruction("mov rcx, QWORD PTR [rsp + 88]"); // lpFileName = wide input + emitter.instruction("xor edx, edx"); // dwDesiredAccess = metadata-only + emitter.instruction("mov r8d, 7"); // share read/write/delete so the current directory and live files resolve + emitter.instruction("xor r9, r9"); // lpSecurityAttributes = NULL + emitter.instruction("mov QWORD PTR [rsp + 32], 3"); // dwCreationDisposition = OPEN_EXISTING + emitter.instruction("mov QWORD PTR [rsp + 40], 0x2000080"); // dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS + emitter.instruction("mov QWORD PTR [rsp + 48], 0"); // hTemplateFile = NULL + emitter.instruction("call CreateFileW"); // open the object so final-path resolution follows symlinks and junctions + emitter.instruction("cmp rax, -1"); // INVALID_HANDLE_VALUE? + emitter.instruction("je .Lrealpath_open_fail"); // native open failure maps to PHP false + emitter.instruction("mov QWORD PTR [rsp + 64], rax"); // preserve handle until final path retrieval completes + emitter.instruction("mov rcx, rax"); // hFile = opened target object + emitter.instruction("mov rdx, QWORD PTR [rsp + 80]"); // lpszFilePath = wide output buffer + emitter.instruction("mov r8d, 32768"); // cchFilePath = WCHAR output capacity + emitter.instruction("xor r9d, r9d"); // FILE_NAME_NORMALIZED | VOLUME_NAME_DOS + emitter.instruction("call GetFinalPathNameByHandleW"); // resolve the physical final path through the opened handle + emitter.instruction("mov DWORD PTR [rsp + 72], eax"); // preserve WCHAR result length across CloseHandle + emitter.instruction("test eax, eax"); // final-path query succeeded? + emitter.instruction("jnz .Lrealpath_query_ok"); // close then process the result + emitter.instruction("call GetLastError"); // capture query failure before closing the handle + emitter.instruction("mov DWORD PTR [rsp + 56], eax"); // retain native error across CloseHandle + emitter.instruction("mov rcx, QWORD PTR [rsp + 64]"); // opened target handle + emitter.instruction("call CloseHandle"); // close on query failure + emitter.instruction("jmp .Lrealpath_native_fail_saved"); // release owned path buffers with the saved error + emitter.label(".Lrealpath_query_ok"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 64]"); // opened target handle + emitter.instruction("call CloseHandle"); // close after successful final-path lookup + emitter.instruction("mov eax, DWORD PTR [rsp + 72]"); // restore WCHAR result length + emitter.instruction("cmp rax, 32768"); // output exceeded capacity? + emitter.instruction("jae .Lrealpath_range_fail"); // reject truncated result + emitter.instruction("mov rdi, QWORD PTR [rsp + 80]"); // wide resolved path + emitter.instruction("mov rax, QWORD PTR [rdi]"); // first four UTF-16 code units + emitter.instruction("mov r10, 0x005c003f005c005c"); // UTF-16 `\\?\` + emitter.instruction("cmp rax, r10"); // extended prefix present? + emitter.instruction("jne .Lrealpath_prefix_done"); // ordinary path + emitter.instruction("mov rax, QWORD PTR [rdi + 8]"); // possible `UNC\` + emitter.instruction("mov r10, 0x005c0043004e0055"); // UTF-16 UNC marker + emitter.instruction("cmp rax, r10"); // extended UNC path? + emitter.instruction("jne .Lrealpath_drive_prefix"); // extended drive path + emitter.instruction("mov WORD PTR [rdi + 12], 92"); // first UNC slash + emitter.instruction("mov WORD PTR [rdi + 14], 92"); // second UNC slash + emitter.instruction("add rdi, 12"); // source now starts with `\\server` + emitter.instruction("jmp .Lrealpath_prefix_done"); // prefix normalized + emitter.label(".Lrealpath_drive_prefix"); + emitter.instruction("add rdi, 8"); // strip `\\?\` + emitter.label(".Lrealpath_prefix_done"); + emitter.instruction("mov rsi, QWORD PTR [rsp + 96]"); // caller UTF-8 destination + emitter.instruction("mov rdx, 4096"); // caller allocation contract + emitter.instruction("call __rt_win_utf16_to_utf8"); // strict UTF-8 result conversion + emitter.instruction("test eax, eax"); // conversion succeeded and fit? + emitter.instruction("jz .Lrealpath_native_fail"); // capture conversion failure + emitter.instruction("mov rax, QWORD PTR [rsp + 80]"); // owned wide result + emitter.instruction("call __rt_heap_free"); // release result conversion + emitter.instruction("mov rax, QWORD PTR [rsp + 88]"); // owned wide input + emitter.instruction("call __rt_heap_free"); // release input conversion + emitter.instruction("mov rax, QWORD PTR [rsp + 96]"); // return resolved UTF-8 pointer + emitter.instruction("add rsp, 104"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lrealpath_open_fail"); + emitter.instruction("call GetLastError"); // capture the open failure before releasing owned buffers + emitter.instruction("mov DWORD PTR [rsp + 56], eax"); // preserve native error across cleanup + emitter.instruction("jmp .Lrealpath_native_fail_saved"); // release both path buffers and publish errno + emitter.label(".Lrealpath_native_fail"); + emitter.instruction("call GetLastError"); // capture native/conversion failure + emitter.instruction("mov DWORD PTR [rsp + 56], eax"); // preserve native error + emitter.label(".Lrealpath_native_fail_saved"); + emitter.instruction("mov rax, QWORD PTR [rsp + 80]"); // owned wide result + emitter.instruction("call __rt_heap_free"); // release result allocation + emitter.instruction("mov rax, QWORD PTR [rsp + 88]"); // owned wide input + emitter.instruction("call __rt_heap_free"); // release input conversion + emitter.instruction("mov eax, DWORD PTR [rsp + 56]"); // restore native error + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // retain native state + emitter.instruction("call __rt_win32_errno_from_code"); // translate errno + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish errno + emitter.instruction("xor rax, rax"); // return NULL on failure + emitter.instruction("add rsp, 104"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lrealpath_range_fail"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 36"); // ENAMETOOLONG + emitter.instruction("jmp .Lrealpath_cleanup_fail"); // release both allocations + emitter.label(".Lrealpath_alloc_fail"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 12"); // ENOMEM + emitter.instruction("mov rax, QWORD PTR [rsp + 88]"); // owned wide input + emitter.instruction("call __rt_heap_free"); // release input conversion + emitter.instruction("xor eax, eax"); // return NULL + emitter.instruction("add rsp, 104"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lrealpath_cleanup_fail"); + emitter.instruction("mov rax, QWORD PTR [rsp + 80]"); // owned wide result + emitter.instruction("call __rt_heap_free"); // release result allocation + emitter.instruction("mov rax, QWORD PTR [rsp + 88]"); // owned wide input + emitter.instruction("call __rt_heap_free"); // release input conversion + emitter.instruction("xor eax, eax"); // return NULL + emitter.instruction("add rsp, 104"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lrealpath_conversion_fail"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 84"); // EILSEQ + emitter.instruction("xor eax, eax"); // return NULL + emitter.instruction("add rsp, 104"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // chmod: delegate to SetFileAttributesW + emitter.label_global("chmod"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_chmod"); // call SetFileAttributesW shim + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // unlink: delegate to DeleteFileW + emitter.label_global("unlink"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_unlink"); // call DeleteFileW shim + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // access: delegate to __rt_sys_access (GetFileAttributesW) + emitter.label_global("access"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_access"); // call GetFileAttributesW shim + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // ftruncate: delegate to __rt_sys_ftruncate (SetFilePointerEx + SetEndOfFile) + emitter.label_global("ftruncate"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_ftruncate"); // call SetFilePointerEx+SetEndOfFile shim + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // umask: php-src keeps process-local mask state on Windows even though the + // filesystem can only reflect its write-bit subset through READONLY. + emitter.label_global("umask"); + emitter.instruction("mov eax, DWORD PTR [rip + __rt_win_umask]"); // return the previous process-local mask + emitter.instruction("mov DWORD PTR [rip + __rt_win_umask], edi"); // publish the caller's new mask + emitter.instruction("ret"); // return + emitter.blank(); + + // sleep: convert SysV `sleep(unsigned seconds)` to Win32 `Sleep(DWORD ms)`. + // libc `sleep` returns 0 when not interrupted by a signal; Win32 `Sleep` has no + // early-wakeup contract here, so we always return 0. + emitter.label_global("sleep"); + // -- frame: shadow(32) + pad(8), 40 ≡ 8 mod 16 keeps rsp ≡ 0 at the call -- + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) for Sleep call + emitter.instruction("imul rcx, rdi, 1000"); // seconds (SysV arg1, rdi) → milliseconds for Win32 Sleep + emitter.instruction("call Sleep"); // Sleep(ms) — blocks the current thread, no return value used + emitter.instruction("xor eax, eax"); // libc sleep returns 0 (no signal interruption on Windows) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return 0 + emitter.blank(); + + // usleep: convert SysV `usleep(useconds_t usec)` to Win32 `Sleep(DWORD ms)`. + // libc `usleep` returns 0 on success; Win32 `Sleep` has no early-wakeup contract + // here, so we always return 0. `usleep(0)` → `Sleep(0)` yields the timeslice, + // matching POSIX semantics. + emitter.label_global("usleep"); + // -- frame: shadow(32) + pad(8), 40 ≡ 8 mod 16 keeps rsp ≡ 0 at the call -- + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) for Sleep call + emitter.instruction("mov rax, rdi"); // microseconds (SysV arg1, rdi) → rax for division + emitter.instruction("xor rdx, rdx"); // clear high half of dividend before unsigned div + emitter.instruction("mov ecx, 1000"); // divisor: 1000 (usec → ms) + emitter.instruction("div rcx"); // rax = usec / 1000 = milliseconds, rdx = remainder + emitter.instruction("mov rcx, rax"); // milliseconds for Win32 Sleep + emitter.instruction("call Sleep"); // Sleep(ms) — blocks the current thread, no return value used + emitter.instruction("xor eax, eax"); // libc usleep returns 0 on success + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return 0 + emitter.blank(); + + // popen: convert SysV `popen(command, mode)` to msvcrt `_popen`. + // SysV: rdi=command, rsi=mode → MSx64: rcx=command, rdx=mode. Returns FILE* in rax. + emitter.label_global("popen"); + // -- popen: SysV→MSx64 for msvcrt _popen -- + emitter.instruction("mov rcx, rdi"); // command (SysV arg1) → MSx64 arg1 + emitter.instruction("mov rdx, rsi"); // mode (SysV arg2) → MSx64 arg2 + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) for _popen call + emitter.instruction("call _popen"); // _popen(command, mode) → FILE* in rax + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return FILE* + emitter.blank(); + + // pclose: convert SysV `pclose(FILE*)` to msvcrt `_pclose`. + // SysV: rdi=stream → MSx64: rcx=stream. Returns int in eax. + emitter.label_global("pclose"); + // -- pclose: SysV→MSx64 for msvcrt _pclose -- + emitter.instruction("mov rcx, rdi"); // stream (SysV arg1) → MSx64 arg1 + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) for _pclose call + emitter.instruction("call _pclose"); // _pclose(stream) → int in eax + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return int + emitter.blank(); + + // fileno: convert SysV `fileno(FILE*)` to msvcrt `_fileno`. + // SysV: rdi=stream → MSx64: rcx=stream. Returns int in eax. + emitter.label_global("fileno"); + // -- fileno: SysV→MSx64 for msvcrt _fileno -- + emitter.instruction("mov rcx, rdi"); // stream (SysV arg1) → MSx64 arg1 + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) for _fileno call + emitter.instruction("call _fileno"); // _fileno(stream) → int in eax + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return int + emitter.blank(); + + // fgetc: convert SysV `fgetc(FILE*)` to msvcrt `fgetc`. + // SysV: rdi=stream → MSx64: rcx=stream. Returns int in eax (char or EOF). + emitter.label_global("__rt_sys_fgetc"); + // -- fgetc: SysV→MSx64 for msvcrt fgetc -- + emitter.instruction("mov rcx, rdi"); // stream (SysV arg1) → MSx64 arg1 + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) for fgetc call + emitter.instruction("call QWORD PTR [rip + __imp_fgetc]"); // imported msvcrt fgetc(stream) → int in eax + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return int + emitter.blank(); + + // system: convert SysV `system(command)` to msvcrt `system`. + // SysV: rdi=command → MSx64: rcx=command. Returns int in eax. + emitter.label_global("__rt_sys_system"); + // -- system: SysV→MSx64 for msvcrt system -- + emitter.instruction("mov rcx, rdi"); // command (SysV arg1) → MSx64 arg1 + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) for system call + emitter.instruction("call QWORD PTR [rip + __imp_system]"); // imported msvcrt system(command) → int in eax + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return int + emitter.blank(); + + // mkdir: delegate to CreateDirectoryW + emitter.label_global("mkdir"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_mkdir"); // call CreateDirectoryW shim + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // rmdir: delegate to RemoveDirectoryW + emitter.label_global("rmdir"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_rmdir"); // call RemoveDirectoryW shim + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // rename: delegate to __rt_sys_rename (MoveFileExW, returns POSIX status) + emitter.label_global("rename"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_rename"); // call MoveFileExW shim (POSIX status) + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // getcwd: delegate to dynamic GetCurrentDirectoryW + strict UTF-8 conversion + emitter.label_global("getcwd"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_getcwd"); // call Unicode getcwd shim + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // chdir: delegate to SetCurrentDirectoryW + emitter.label_global("chdir"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_chdir"); // call SetCurrentDirectoryW shim + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // stat: delegate to msvcrt stat + emitter.label_global("stat"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("call __rt_sys_stat"); // call msvcrt stat + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); + + // dirfd: Windows has no kernel concept of "the fd behind this DIR*" (the + // DIR* minted by `__rt_sys_opendir` is a heap pointer, far too large to + // index the 256-slot `_dir_handles` table `opendir.rs` keys by fd). Mint + // a small, distinct table index the same way `opendir_glob.rs` already + // does for `_glob_handles`: `dup(2)` msvcrt's CRT-level fd for stderr via + // `__rt_sys_dup` (backed by `_dup`, which hands out small increasing CRT + // fds independent of any Win32 HANDLE value). The DIR* itself is ignored + // here — `opendir.rs` stores it in `_dir_handles[fd]` right after this + // call returns, so the fd only needs to be a fresh, in-range table key. + // hstrerror: return NULL — use WSAGetLastError instead + // h_errno: return 0 — Windows uses WSAGetLastError + emitter.label_global("dirfd"); + emitter.instruction("sub rsp, 8"); // align to 16 bytes before calling __rt_sys_dup + emitter.instruction("mov rdi, 2"); // dup stderr's CRT fd — mints a fresh small table-index fd + emitter.instruction("call __rt_sys_dup"); // rax = new small fd (or -1 on exhaustion, matching libc dirfd's rare failure mode) + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return the minted fd + emitter.blank(); + emitter.label_global("hstrerror"); + emitter.instruction("xor rax, rax"); // return NULL + emitter.instruction("ret"); // return + emitter.blank(); + emitter.label_global("h_errno"); + emitter.instruction("xor rax, rax"); // return 0 + emitter.instruction("ret"); // return + emitter.blank(); + + // timegm: delegate to msvcrt _mkgmtime + emitter.label_global("timegm"); + emitter.instruction("sub rsp, 40"); // shadow space + emitter.instruction("mov rcx, rdi"); // struct tm pointer + emitter.instruction("call _mkgmtime"); // convert UTC tm to time_t + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return time_t in rax + emitter.blank(); + + // __errno static variable — see __rt_win32_errno_from_code below for who + // writes it and the docblock on __errno_location above for who reads it. + emitter.raw(".data"); + emitter.raw("__rt_errno:"); + emitter.raw(" .zero 8"); + emitter.raw("__rt_win_umask:"); + emitter.raw(" .zero 4"); + emitter.raw("__rt_win32_last_error:"); + emitter.raw(" .zero 4"); + emitter.raw("__rt_wsa_last_error:"); + emitter.raw(" .zero 4"); + emitter.raw(".text"); + emitter.blank(); + + emit_win32_errno_translate(emitter); +} + +/// Translates a Win32 `GetLastError`/`WSAGetLastError` code into the POSIX +/// errno value the runtime's read/recv consumers understand. Windows unifies +/// the two: `WSAGetLastError` is a thin wrapper over the same per-thread +/// `GetLastError` value, so one table serves both the ReadFile (kernel32) +/// caller (`shims_fs.rs`'s `__rt_sys_read`) and the Winsock recv callers +/// (`shims_net.rs`'s `__rt_sys_recvfrom`/`__rt_sys_recvmsg`). +/// +/// Contract: EAX in = Win32/WSA error code, EAX out = POSIX errno. Leaf +/// helper (issues no further calls); only EAX/ECX are live across it. +/// +/// Critical — must map to EAGAIN(11), the `fgets`/`fread` would-block check +/// (`io/fgets.rs`, `io/fread.rs`, `cmp r10d, 11`): `WSAEWOULDBLOCK`(10035), +/// `ERROR_NO_DATA`(232), `ERROR_IO_PENDING`(997). +/// +/// Common: `ERROR_FILE_NOT_FOUND`(2)/`ERROR_PATH_NOT_FOUND`(3) -> ENOENT(2); +/// `ERROR_ACCESS_DENIED`(5) -> EACCES(13); `ERROR_INVALID_HANDLE`(6) -> +/// EBADF(9); `ERROR_BROKEN_PIPE`(109) -> EPIPE(32); `ERROR_HANDLE_EOF`(38) +/// -> 0 (EOF is not an error); `WSAEINTR`(10004) -> EINTR(4); +/// `ERROR_INSUFFICIENT_BUFFER`(122) -> ERANGE(34); +/// `ERROR_NO_UNICODE_TRANSLATION`(1113) -> EILSEQ(84); +/// `WSAECONNRESET`(10054) -> ECONNRESET(104); `WSAETIMEDOUT`(10060) -> +/// ETIMEDOUT(110). Any other code -> EIO(5). +/// Emits exact Win32 `readlink` lowering through `FSCTL_GET_REPARSE_POINT`. +fn emit_shim_readlink_reparse(emitter: &mut Emitter) { + emitter.label_global("readlink"); + emitter.instruction("sub rsp, 152"); // DeviceIoControl stack args and owned conversion locals + emitter.instruction("mov QWORD PTR [rsp + 88], rsi"); // caller byte destination + emitter.instruction("mov QWORD PTR [rsp + 96], rdx"); // caller byte capacity + emitter.instruction("call __rt_win_utf8_to_utf16"); // convert reparse-point path strictly + emitter.instruction("test rax, rax"); // conversion succeeded? + emitter.instruction("jz .Lreadlink_exact_conversion_fail"); // invalid UTF-8 + emitter.instruction("mov QWORD PTR [rsp + 64], rax"); // owned wide input path + emitter.instruction("mov rax, 16384"); // MAXIMUM_REPARSE_DATA_BUFFER_SIZE + emitter.instruction("call __rt_heap_alloc"); // allocate REPARSE_DATA_BUFFER + emitter.instruction("test rax, rax"); // allocation succeeded? + emitter.instruction("jz .Lreadlink_exact_alloc_reparse_fail"); // cleanup input + emitter.instruction("mov QWORD PTR [rsp + 72], rax"); // owned reparse buffer + emitter.instruction("mov rcx, QWORD PTR [rsp + 64]"); // wide link path + emitter.instruction("xor edx, edx"); // metadata-only access + emitter.instruction("mov r8, 7"); // share read/write/delete + emitter.instruction("xor r9, r9"); // security attributes = NULL + emitter.instruction("mov QWORD PTR [rsp + 32], 3"); // OPEN_EXISTING + emitter.instruction("mov QWORD PTR [rsp + 40], 0x2200000"); // BACKUP_SEMANTICS | OPEN_REPARSE_POINT + emitter.instruction("mov QWORD PTR [rsp + 48], 0"); // template = NULL + emitter.instruction("call CreateFileW"); // open reparse point without following it + emitter.instruction("cmp rax, -1"); // INVALID_HANDLE_VALUE? + emitter.instruction("je .Lreadlink_exact_native_fail"); // capture open failure + emitter.instruction("mov QWORD PTR [rsp + 80], rax"); // retain handle + emitter.instruction("mov rcx, rax"); // DeviceIoControl handle + emitter.instruction("mov edx, 0x900A8"); // FSCTL_GET_REPARSE_POINT + emitter.instruction("xor r8, r8"); // no input buffer + emitter.instruction("xor r9d, r9d"); // input size = 0 + emitter.instruction("mov rax, QWORD PTR [rsp + 72]"); // reparse output buffer + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // lpOutBuffer (arg5) + emitter.instruction("mov QWORD PTR [rsp + 40], 16384"); // nOutBufferSize (arg6) + emitter.instruction("lea rax, [rsp + 104]"); // bytes-returned slot + emitter.instruction("mov QWORD PTR [rsp + 48], rax"); // lpBytesReturned (arg7) + emitter.instruction("mov QWORD PTR [rsp + 56], 0"); // lpOverlapped = NULL (arg8) + emitter.instruction("call DeviceIoControl"); // fetch REPARSE_DATA_BUFFER + emitter.instruction("test eax, eax"); // ioctl succeeded? + emitter.instruction("jnz .Lreadlink_exact_ioctl_ok"); // parse returned buffer + emitter.instruction("call GetLastError"); // capture ioctl failure before close + emitter.instruction("mov DWORD PTR [rsp + 136], eax"); // preserve native error + emitter.instruction("mov rcx, QWORD PTR [rsp + 80]"); // opened handle + emitter.instruction("call CloseHandle"); // close failed ioctl handle + emitter.instruction("jmp .Lreadlink_exact_native_fail_saved"); // cleanup with saved error + emitter.label(".Lreadlink_exact_ioctl_ok"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 80]"); // opened handle + emitter.instruction("call CloseHandle"); // close after successful ioctl + emitter.instruction("cmp DWORD PTR [rsp + 104], 8"); // fixed reparse header present? + emitter.instruction("jb .Lreadlink_exact_invalid_reparse"); // truncated header + emitter.instruction("mov r10, QWORD PTR [rsp + 72]"); // REPARSE_DATA_BUFFER base + emitter.instruction("mov eax, DWORD PTR [r10]"); // ReparseTag + emitter.instruction("cmp eax, 0xA000000C"); // IO_REPARSE_TAG_SYMLINK + emitter.instruction("je .Lreadlink_exact_symlink"); // symlink layout has PathBuffer at +20 + emitter.instruction("cmp eax, 0xA0000003"); // IO_REPARSE_TAG_MOUNT_POINT + emitter.instruction("je .Lreadlink_exact_mount"); // junction layout has PathBuffer at +16 + emitter.instruction("jmp .Lreadlink_exact_invalid_reparse"); // unsupported reparse tag + emitter.label(".Lreadlink_exact_symlink"); + emitter.instruction("cmp DWORD PTR [rsp + 104], 20"); // symbolic-link fixed fields present? + emitter.instruction("jb .Lreadlink_exact_invalid_reparse"); // truncated symbolic-link record + emitter.instruction("lea r11, [r10 + 20]"); // symbolic-link PathBuffer + emitter.instruction("jmp .Lreadlink_exact_select_name"); // shared name selection + emitter.label(".Lreadlink_exact_mount"); + emitter.instruction("cmp DWORD PTR [rsp + 104], 16"); // mount-point fixed fields present? + emitter.instruction("jb .Lreadlink_exact_invalid_reparse"); // truncated mount-point record + emitter.instruction("lea r11, [r10 + 16]"); // junction PathBuffer + emitter.label(".Lreadlink_exact_select_name"); + emitter.instruction("movzx eax, WORD PTR [r10 + 14]"); // PrintNameLength in bytes + emitter.instruction("movzx ecx, WORD PTR [r10 + 12]"); // PrintNameOffset in bytes + emitter.instruction("test eax, eax"); // printable name available? + emitter.instruction("jnz .Lreadlink_exact_name_selected"); // prefer print name + emitter.instruction("movzx eax, WORD PTR [r10 + 10]"); // SubstituteNameLength in bytes + emitter.instruction("movzx ecx, WORD PTR [r10 + 8]"); // SubstituteNameOffset in bytes + emitter.label(".Lreadlink_exact_name_selected"); + emitter.instruction("test eax, eax"); // selected name non-empty? + emitter.instruction("jz .Lreadlink_exact_invalid_reparse"); // malformed reparse record + emitter.instruction("test eax, 1"); // UTF-16 byte length must be even + emitter.instruction("jnz .Lreadlink_exact_invalid_reparse"); // reject half code unit + emitter.instruction("test ecx, 1"); // UTF-16 byte offset must be even + emitter.instruction("jnz .Lreadlink_exact_invalid_reparse"); // reject misaligned slice + emitter.instruction("lea rdx, [r11 + rcx]"); // selected slice start + emitter.instruction("lea r8, [rdx + rax]"); // selected slice end + emitter.instruction("mov r9d, DWORD PTR [rsp + 104]"); // bytes returned by DeviceIoControl + emitter.instruction("lea r9, [r10 + r9]"); // end of initialized reparse bytes + emitter.instruction("cmp r8, r9"); // selected name lies inside returned data? + emitter.instruction("ja .Lreadlink_exact_invalid_reparse"); // reject out-of-bounds offset/length + emitter.instruction("lea r9, [r10 + 16382]"); // last safe location for a WCHAR terminator + emitter.instruction("cmp r8, r9"); // terminator fits allocation? + emitter.instruction("ja .Lreadlink_exact_invalid_reparse"); // reject allocation overflow + emitter.instruction("lea rdi, [r11 + rcx]"); // selected UTF-16 slice + emitter.instruction("mov WORD PTR [rdi + rax], 0"); // terminate selected byte-length slice + emitter.instruction("mov rax, QWORD PTR [rdi]"); // inspect first four UTF-16 code units + emitter.instruction("mov r10, 0x005c003f005c005c"); // `\\?\` prefix + emitter.instruction("cmp rax, r10"); // Win32 extended prefix? + emitter.instruction("je .Lreadlink_exact_extended_prefix"); // normalize extended form + emitter.instruction("mov r10, 0x005c003f003f005c"); // NT `\??\` prefix + emitter.instruction("cmp rax, r10"); // NT substitute prefix? + emitter.instruction("jne .Lreadlink_exact_prefix_done"); // relative/ordinary print name + emitter.label(".Lreadlink_exact_extended_prefix"); + emitter.instruction("mov rax, QWORD PTR [rdi + 8]"); // possible `UNC\` marker + emitter.instruction("mov r10, 0x005c0043004e0055"); // UTF-16 `UNC\` + emitter.instruction("cmp rax, r10"); // UNC substitution? + emitter.instruction("jne .Lreadlink_exact_drive_prefix"); // drive path: strip four units + emitter.instruction("mov WORD PTR [rdi + 12], 92"); // synthesize first UNC slash + emitter.instruction("mov WORD PTR [rdi + 14], 92"); // synthesize second UNC slash + emitter.instruction("add rdi, 12"); // source begins `\\server` + emitter.instruction("jmp .Lreadlink_exact_prefix_done"); // normalized UNC + emitter.label(".Lreadlink_exact_drive_prefix"); + emitter.instruction("add rdi, 8"); // strip four UTF-16 prefix units + emitter.label(".Lreadlink_exact_prefix_done"); + emitter.instruction("mov QWORD PTR [rsp + 112], rdi"); // preserve selected wide source + emitter.instruction("xor esi, esi"); // query UTF-8 size + emitter.instruction("xor edx, edx"); // no destination buffer + emitter.instruction("call __rt_win_utf16_to_utf8"); // required bytes including NUL + emitter.instruction("test eax, eax"); // conversion size query succeeded? + emitter.instruction("jz .Lreadlink_exact_native_fail"); // capture conversion failure + emitter.instruction("mov DWORD PTR [rsp + 128], eax"); // exact UTF-8 allocation size + emitter.instruction("movsxd rax, eax"); // widen byte count + emitter.instruction("call __rt_heap_alloc"); // allocate temporary UTF-8 result + emitter.instruction("test rax, rax"); // allocation succeeded? + emitter.instruction("jz .Lreadlink_exact_alloc_utf8_fail"); // cleanup wide/reparse buffers + emitter.instruction("mov QWORD PTR [rsp + 120], rax"); // owned UTF-8 temporary + emitter.instruction("mov rdi, QWORD PTR [rsp + 112]"); // selected wide source + emitter.instruction("mov rsi, rax"); // UTF-8 temporary destination + emitter.instruction("mov edx, DWORD PTR [rsp + 128]"); // exact byte capacity + emitter.instruction("call __rt_win_utf16_to_utf8"); // strict conversion + emitter.instruction("test eax, eax"); // conversion succeeded? + emitter.instruction("jz .Lreadlink_exact_native_fail_utf8"); // cleanup all allocations + emitter.instruction("dec eax"); // payload bytes exclude NUL + emitter.instruction("mov rcx, QWORD PTR [rsp + 96]"); // caller capacity + emitter.instruction("cmp rax, rcx"); // payload fits caller buffer? + emitter.instruction("cmova rax, rcx"); // POSIX readlink truncates to capacity + emitter.instruction("mov QWORD PTR [rsp + 136], rax"); // preserve return byte count + emitter.instruction("mov rcx, rax"); // copy count + emitter.instruction("mov rsi, QWORD PTR [rsp + 120]"); // temporary UTF-8 source + emitter.instruction("mov rdi, QWORD PTR [rsp + 88]"); // caller destination + emitter.instruction("cld"); // copy forward regardless of prior string operations + emitter.instruction("rep movsb"); // copy exactly returned bytes, no forced NUL + emitter.instruction("jmp .Lreadlink_exact_cleanup_success"); // balanced cleanup + emitter.label(".Lreadlink_exact_cleanup_success"); + emitter.instruction("mov rax, QWORD PTR [rsp + 120]"); // UTF-8 temporary + emitter.instruction("call __rt_heap_free"); // release temporary result + emitter.instruction("mov rax, QWORD PTR [rsp + 72]"); // reparse buffer + emitter.instruction("call __rt_heap_free"); // release reparse data + emitter.instruction("mov rax, QWORD PTR [rsp + 64]"); // wide input path + emitter.instruction("call __rt_heap_free"); // release input conversion + emitter.instruction("mov rax, QWORD PTR [rsp + 136]"); // return bytes copied + emitter.instruction("add rsp, 152"); // restore stack + emitter.instruction("ret"); // return exact POSIX readlink count + emitter.label(".Lreadlink_exact_native_fail_utf8"); + emitter.instruction("call GetLastError"); // capture final conversion failure + emitter.instruction("mov DWORD PTR [rsp + 136], eax"); // preserve native error + emitter.instruction("mov rax, QWORD PTR [rsp + 120]"); // UTF-8 temporary + emitter.instruction("call __rt_heap_free"); // release failed conversion buffer + emitter.instruction("jmp .Lreadlink_exact_native_fail_saved"); // shared cleanup + emitter.label(".Lreadlink_exact_native_fail"); + emitter.instruction("call GetLastError"); // capture native failure + emitter.instruction("mov DWORD PTR [rsp + 136], eax"); // preserve native error + emitter.label(".Lreadlink_exact_native_fail_saved"); + emitter.instruction("mov rax, QWORD PTR [rsp + 72]"); // reparse buffer + emitter.instruction("call __rt_heap_free"); // release reparse data + emitter.instruction("mov rax, QWORD PTR [rsp + 64]"); // wide input path + emitter.instruction("call __rt_heap_free"); // release input conversion + emitter.instruction("mov eax, DWORD PTR [rsp + 136]"); // restore native error + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // retain native state + emitter.instruction("call __rt_win32_errno_from_code"); // translate errno + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish errno + emitter.instruction("mov rax, -1"); // return failure + emitter.instruction("add rsp, 152"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lreadlink_exact_invalid_reparse"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 22"); // EINVAL for unsupported/malformed tag + emitter.instruction("jmp .Lreadlink_exact_cleanup_errno"); // cleanup owned buffers + emitter.label(".Lreadlink_exact_alloc_utf8_fail"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 12"); // ENOMEM + emitter.label(".Lreadlink_exact_cleanup_errno"); + emitter.instruction("mov rax, QWORD PTR [rsp + 72]"); // reparse buffer + emitter.instruction("call __rt_heap_free"); // release reparse data + emitter.instruction("mov rax, QWORD PTR [rsp + 64]"); // wide input path + emitter.instruction("call __rt_heap_free"); // release input conversion + emitter.instruction("mov rax, -1"); // return failure + emitter.instruction("add rsp, 152"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lreadlink_exact_alloc_reparse_fail"); + emitter.instruction("mov rax, QWORD PTR [rsp + 64]"); // wide input path + emitter.instruction("call __rt_heap_free"); // release input conversion + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 12"); // ENOMEM + emitter.instruction("mov rax, -1"); // return failure + emitter.instruction("add rsp, 152"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lreadlink_exact_conversion_fail"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 84"); // EILSEQ + emitter.instruction("mov rax, -1"); // return failure + emitter.instruction("add rsp, 152"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits a tag-specific `lstat()` reparse-point probe. +/// +/// The probe first excludes ordinary paths with `GetFileAttributesW`, then +/// opens a reparse point with `FILE_FLAG_OPEN_REPARSE_POINT` and reads its +/// `REPARSE_DATA_BUFFER`. It returns one only for `IO_REPARSE_TAG_SYMLINK`. +/// A junction (`IO_REPARSE_TAG_MOUNT_POINT`) and every other reparse point +/// return zero. A failed `DeviceIoControl` on an opened reparse point returns +/// -1 after publishing errno, matching php-src's `lstat` error propagation +/// rather than silently falling back to `stat`. +fn emit_shim_lstat_symlink_probe(emitter: &mut Emitter) { + emitter.label_global("__rt_lstat_is_symlink"); + emitter.instruction("sub rsp, 152"); // shadow space, Win32 stack arguments, and owned wide/reparse buffers + emitter.instruction("call __rt_win_utf8_to_utf16"); // convert the SysV UTF-8 path without lossy ANSI fallback + emitter.instruction("test rax, rax"); // conversion succeeded? + emitter.instruction("jz .Llstat_probe_false"); // invalid UTF-8 cannot be a symbolic link + emitter.instruction("mov QWORD PTR [rsp + 64], rax"); // retain the owned wide path across probing calls + emitter.instruction("mov rcx, rax"); // inspect the converted path's native attributes first + emitter.instruction("call GetFileAttributesW"); // ordinary files must not enter FSCTL_GET_REPARSE_POINT + emitter.instruction("cmp eax, -1"); // inaccessible paths fall through to the normal lstat() failure path + emitter.instruction("je .Llstat_probe_cleanup_wide_false"); // let stat() report the original path error + emitter.instruction("test eax, 0x400"); // FILE_ATTRIBUTE_REPARSE_POINT set? + emitter.instruction("jz .Llstat_probe_cleanup_wide_false"); // regular files and directories use lstat's normal stat fallback + emitter.instruction("mov rax, 16384"); // MAXIMUM_REPARSE_DATA_BUFFER_SIZE + emitter.instruction("call __rt_heap_alloc"); // allocate a complete REPARSE_DATA_BUFFER + emitter.instruction("test rax, rax"); // allocation succeeded? + emitter.instruction("jz .Llstat_probe_cleanup_wide_false"); // conservative non-link result on allocation failure + emitter.instruction("mov QWORD PTR [rsp + 72], rax"); // retain the owned reparse buffer + emitter.instruction("mov rcx, QWORD PTR [rsp + 64]"); // lpFileName = wide source path + emitter.instruction("xor edx, edx"); // metadata-only access is sufficient for the control query + emitter.instruction("mov r8, 7"); // permit concurrent readers, writers, and deleters + emitter.instruction("xor r9, r9"); // lpSecurityAttributes = NULL + emitter.instruction("mov QWORD PTR [rsp + 32], 3"); // dwCreationDisposition = OPEN_EXISTING + emitter.instruction("mov QWORD PTR [rsp + 40], 0x2200000"); // FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT + emitter.instruction("mov QWORD PTR [rsp + 48], 0"); // hTemplateFile = NULL + emitter.instruction("call CreateFileW"); // open the reparse point itself rather than its target + emitter.instruction("cmp rax, -1"); // INVALID_HANDLE_VALUE? + emitter.instruction("je .Llstat_probe_cleanup_false"); // inaccessible/non-reparse paths are not marked as links + emitter.instruction("mov QWORD PTR [rsp + 80], rax"); // preserve the opened handle through DeviceIoControl + emitter.instruction("mov rcx, rax"); // hDevice = opened reparse point + emitter.instruction("mov edx, 0x900A8"); // dwIoControlCode = FSCTL_GET_REPARSE_POINT + emitter.instruction("xor r8, r8"); // lpInBuffer = NULL + emitter.instruction("xor r9d, r9d"); // nInBufferSize = 0 + emitter.instruction("mov rax, QWORD PTR [rsp + 72]"); // allocated REPARSE_DATA_BUFFER + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // lpOutBuffer (fifth MSx64 argument) + emitter.instruction("mov QWORD PTR [rsp + 40], 16384"); // nOutBufferSize = MAXIMUM_REPARSE_DATA_BUFFER_SIZE + emitter.instruction("lea rax, [rsp + 104]"); // bytes-returned storage + emitter.instruction("mov QWORD PTR [rsp + 48], rax"); // lpBytesReturned (seventh MSx64 argument) + emitter.instruction("mov QWORD PTR [rsp + 56], 0"); // lpOverlapped = NULL + emitter.instruction("call DeviceIoControl"); // fetch the reparse tag from the opened object + emitter.instruction("mov DWORD PTR [rsp + 136], eax"); // retain control success across CloseHandle + emitter.instruction("test eax, eax"); // DeviceIoControl succeeded? + emitter.instruction("jnz .Llstat_probe_ioctl_ok"); // inspect the returned reparse tag + emitter.instruction("call GetLastError"); // capture reparse-control failure before closing its handle + emitter.instruction("mov DWORD PTR [rsp + 136], eax"); // preserve native error through cleanup + emitter.instruction("mov rcx, QWORD PTR [rsp + 80]"); // opened reparse-point handle + emitter.instruction("call CloseHandle"); // close failed control-query handle before returning errno + emitter.instruction("jmp .Llstat_probe_cleanup_error"); // free probe buffers and propagate failure to lstat + emitter.label(".Llstat_probe_ioctl_ok"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 80]"); // opened reparse-point handle + emitter.instruction("call CloseHandle"); // close successful control-query handle + emitter.instruction("cmp DWORD PTR [rsp + 104], 8"); // REPARSE_DATA_BUFFER header is complete? + emitter.instruction("jb .Llstat_probe_cleanup_false"); // malformed replies are not symbolic links + emitter.instruction("mov rax, QWORD PTR [rsp + 72]"); // REPARSE_DATA_BUFFER base + emitter.instruction("cmp DWORD PTR [rax], 0xA000000C"); // IO_REPARSE_TAG_SYMLINK only + emitter.instruction("sete al"); // return one for symlinks and zero for junctions/other tags + emitter.instruction("movzx eax, al"); // normalize the Boolean return value + emitter.instruction("mov DWORD PTR [rsp + 136], eax"); // preserve the classification through heap cleanup + emitter.instruction("jmp .Llstat_probe_cleanup_result"); // release owned probe storage + emitter.label(".Llstat_probe_cleanup_false"); + emitter.instruction("xor eax, eax"); // default to non-link for failed/unsupported probes + emitter.instruction("mov DWORD PTR [rsp + 136], eax"); // preserve false through heap cleanup + emitter.label(".Llstat_probe_cleanup_result"); + emitter.instruction("mov rax, QWORD PTR [rsp + 72]"); // owned reparse buffer + emitter.instruction("call __rt_heap_free"); // release REPARSE_DATA_BUFFER + emitter.instruction("mov rax, QWORD PTR [rsp + 64]"); // owned wide input path + emitter.instruction("call __rt_heap_free"); // release UTF-16 conversion + emitter.instruction("mov eax, DWORD PTR [rsp + 136]"); // restore Boolean classification + emitter.instruction("add rsp, 152"); // restore the SysV caller stack + emitter.instruction("ret"); // return one only for IO_REPARSE_TAG_SYMLINK + emitter.label(".Llstat_probe_cleanup_error"); + emitter.instruction("mov rax, QWORD PTR [rsp + 72]"); // owned reparse buffer after failed control query + emitter.instruction("call __rt_heap_free"); // release REPARSE_DATA_BUFFER + emitter.instruction("mov rax, QWORD PTR [rsp + 64]"); // owned wide input path + emitter.instruction("call __rt_heap_free"); // release UTF-16 conversion + emitter.instruction("mov eax, DWORD PTR [rsp + 136]"); // restore native DeviceIoControl error + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // retain diagnostic state for callers + emitter.instruction("call __rt_win32_errno_from_code"); // translate native reparse-control error to POSIX errno + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish errno for lstat() + emitter.instruction("mov rax, -1"); // tri-state failure result + emitter.instruction("add rsp, 152"); // restore the SysV caller stack + emitter.instruction("ret"); // return failed reparse classification + emitter.label(".Llstat_probe_cleanup_wide_false"); + emitter.instruction("mov rax, QWORD PTR [rsp + 64]"); // owned wide input after allocation failure + emitter.instruction("call __rt_heap_free"); // release UTF-16 conversion + emitter.label(".Llstat_probe_false"); + emitter.instruction("xor eax, eax"); // failed conversion/allocation is conservatively not a link + emitter.instruction("add rsp, 152"); // restore the SysV caller stack + emitter.instruction("ret"); // return false + emitter.blank(); +} + +/// Emits conversion of a stack-resident Win32 `FILETIME` to the Unix epoch +/// seconds field of the Linux-layout `struct stat` currently based in `rsi`. +/// +/// `BY_HANDLE_FILE_INFORMATION` stores 100-nanosecond ticks since 1601. The +/// subtraction and signed division preserve valid pre-1970 link timestamps; +/// all clobbered registers are volatile under the MSx64 calls already complete +/// on the surrounding `lstat` path. +fn emit_lstat_filetime_seconds(emitter: &mut Emitter, src_off: usize, dst_off: usize) { + emitter.instruction(&format!("mov rax, QWORD PTR [rsp + {}]", src_off)); // load the 64-bit FILETIME tick count + emitter.instruction("mov r8, 116444736000000000"); // 1601-to-1970 offset in 100-nanosecond ticks + emitter.instruction("sub rax, r8"); // rebase onto the Unix epoch + emitter.instruction("cqo"); // sign-extend pre-1970 deltas for signed division + emitter.instruction("mov r9, 10000000"); // FILETIME ticks per whole second + emitter.instruction("idiv r9"); // rax = Unix epoch seconds + emitter.instruction(&format!("mov QWORD PTR [rsi + {}], rax", dst_off)); // store the stat timestamp seconds +} + +/// Emits the shared Win32 and Winsock error-code to POSIX `errno` translator. +pub(super) fn emit_win32_errno_translate(emitter: &mut Emitter) { + emitter.label_global("__rt_win32_errno_from_code"); + emitter.instruction("mov ecx, eax"); // save the raw Win32/WSA code for the compare chain + emitter.instruction("cmp ecx, 10035"); // WSAEWOULDBLOCK + emitter.instruction("je .Lw32err_eagain"); // -> EAGAIN + emitter.instruction("cmp ecx, 232"); // ERROR_NO_DATA (nonblocking pipe read, no data yet) + emitter.instruction("je .Lw32err_eagain"); // -> EAGAIN + emitter.instruction("cmp ecx, 997"); // ERROR_IO_PENDING (overlapped I/O still in flight) + emitter.instruction("je .Lw32err_eagain"); // -> EAGAIN + emitter.instruction("cmp ecx, 2"); // ERROR_FILE_NOT_FOUND + emitter.instruction("je .Lw32err_enoent"); // -> ENOENT + emitter.instruction("cmp ecx, 3"); // ERROR_PATH_NOT_FOUND + emitter.instruction("je .Lw32err_enoent"); // -> ENOENT + emitter.instruction("cmp ecx, 5"); // ERROR_ACCESS_DENIED + emitter.instruction("je .Lw32err_eacces"); // -> EACCES + emitter.instruction("cmp ecx, 6"); // ERROR_INVALID_HANDLE + emitter.instruction("je .Lw32err_ebadf"); // -> EBADF + emitter.instruction("cmp ecx, 109"); // ERROR_BROKEN_PIPE + emitter.instruction("je .Lw32err_epipe"); // -> EPIPE + emitter.instruction("cmp ecx, 122"); // ERROR_INSUFFICIENT_BUFFER + emitter.instruction("je .Lw32err_erange"); // -> ERANGE + emitter.instruction("cmp ecx, 1113"); // ERROR_NO_UNICODE_TRANSLATION + emitter.instruction("je .Lw32err_eilseq"); // -> EILSEQ + emitter.instruction("cmp ecx, 38"); // ERROR_HANDLE_EOF + emitter.instruction("je .Lw32err_eof"); // -> 0 (EOF is not an error) + emitter.instruction("cmp ecx, 10004"); // WSAEINTR + emitter.instruction("je .Lw32err_eintr"); // -> EINTR + emitter.instruction("cmp ecx, 10054"); // WSAECONNRESET + emitter.instruction("je .Lw32err_econnreset"); // -> ECONNRESET + emitter.instruction("cmp ecx, 10060"); // WSAETIMEDOUT + emitter.instruction("je .Lw32err_etimedout"); // -> ETIMEDOUT + emitter.instruction("cmp ecx, 10061"); // WSAECONNREFUSED + emitter.instruction("je .Lw32err_econnrefused"); // -> ECONNREFUSED + emitter.instruction("cmp ecx, 10036"); // WSAEINPROGRESS + emitter.instruction("je .Lw32err_einprogress"); // -> EINPROGRESS + emitter.instruction("cmp ecx, 10037"); // WSAEALREADY + emitter.instruction("je .Lw32err_ealready"); // -> EALREADY + emitter.instruction("cmp ecx, 10038"); // WSAENOTSOCK + emitter.instruction("je .Lw32err_enotsock"); // -> ENOTSOCK + emitter.instruction("cmp ecx, 10048"); // WSAEADDRINUSE + emitter.instruction("je .Lw32err_eaddrinuse"); // -> EADDRINUSE + emitter.instruction("cmp ecx, 10049"); // WSAEADDRNOTAVAIL + emitter.instruction("je .Lw32err_eaddrnotavail"); // -> EADDRNOTAVAIL + emitter.instruction("cmp ecx, 10050"); // WSAENETDOWN + emitter.instruction("je .Lw32err_enetdown"); // -> ENETDOWN + emitter.instruction("cmp ecx, 10051"); // WSAENETUNREACH + emitter.instruction("je .Lw32err_enetunreach"); // -> ENETUNREACH + emitter.instruction("cmp ecx, 10053"); // WSAECONNABORTED + emitter.instruction("je .Lw32err_econnaborted"); // -> ECONNABORTED + emitter.instruction("cmp ecx, 10055"); // WSAENOBUFS + emitter.instruction("je .Lw32err_enobufs"); // -> ENOBUFS + emitter.instruction("cmp ecx, 10057"); // WSAENOTCONN + emitter.instruction("je .Lw32err_enotconn"); // -> ENOTCONN + emitter.instruction("cmp ecx, 112"); // ERROR_DISK_FULL + emitter.instruction("je .Lw32err_enospc"); // -> ENOSPC + emitter.instruction("cmp ecx, 145"); // ERROR_DIR_NOT_EMPTY + emitter.instruction("je .Lw32err_enotempty"); // -> ENOTEMPTY + emitter.instruction("cmp ecx, 183"); // ERROR_ALREADY_EXISTS + emitter.instruction("je .Lw32err_eexist"); // -> EEXIST + emitter.instruction("cmp ecx, 206"); // ERROR_FILENAME_EXCED_RANGE + emitter.instruction("je .Lw32err_enametoolong"); // -> ENAMETOOLONG + emitter.instruction("cmp ecx, 267"); // ERROR_DIRECTORY + emitter.instruction("je .Lw32err_enotdir"); // -> ENOTDIR + emitter.instruction("mov eax, 5"); // unknown code -> EIO + emitter.instruction("ret"); // return + emitter.label(".Lw32err_eagain"); + emitter.instruction("mov eax, 11"); // EAGAIN + emitter.instruction("ret"); // return + emitter.label(".Lw32err_enoent"); + emitter.instruction("mov eax, 2"); // ENOENT + emitter.instruction("ret"); // return + emitter.label(".Lw32err_eacces"); + emitter.instruction("mov eax, 13"); // EACCES + emitter.instruction("ret"); // return + emitter.label(".Lw32err_ebadf"); + emitter.instruction("mov eax, 9"); // EBADF + emitter.instruction("ret"); // return + emitter.label(".Lw32err_epipe"); + emitter.instruction("mov eax, 32"); // EPIPE + emitter.instruction("ret"); // return + emitter.label(".Lw32err_erange"); + emitter.instruction("mov eax, 34"); // ERANGE + emitter.instruction("ret"); // return + emitter.label(".Lw32err_eilseq"); + emitter.instruction("mov eax, 84"); // EILSEQ + emitter.instruction("ret"); // return + emitter.label(".Lw32err_eof"); + emitter.instruction("xor eax, eax"); // EOF is not an error + emitter.instruction("ret"); // return + emitter.label(".Lw32err_eintr"); + emitter.instruction("mov eax, 4"); // EINTR + emitter.instruction("ret"); // return + emitter.label(".Lw32err_econnreset"); + emitter.instruction("mov eax, 104"); // ECONNRESET + emitter.instruction("ret"); // return + emitter.label(".Lw32err_etimedout"); + emitter.instruction("mov eax, 110"); // ETIMEDOUT + emitter.instruction("ret"); // return + emitter.label(".Lw32err_econnrefused"); + emitter.instruction("mov eax, 111"); // ECONNREFUSED + emitter.instruction("ret"); // return + emitter.label(".Lw32err_einprogress"); + emitter.instruction("mov eax, 115"); // EINPROGRESS + emitter.instruction("ret"); // return + emitter.label(".Lw32err_ealready"); + emitter.instruction("mov eax, 114"); // EALREADY + emitter.instruction("ret"); // return + emitter.label(".Lw32err_enotsock"); + emitter.instruction("mov eax, 88"); // ENOTSOCK + emitter.instruction("ret"); // return + emitter.label(".Lw32err_eaddrinuse"); + emitter.instruction("mov eax, 98"); // EADDRINUSE + emitter.instruction("ret"); // return + emitter.label(".Lw32err_eaddrnotavail"); + emitter.instruction("mov eax, 99"); // EADDRNOTAVAIL + emitter.instruction("ret"); // return + emitter.label(".Lw32err_enetdown"); + emitter.instruction("mov eax, 100"); // ENETDOWN + emitter.instruction("ret"); // return + emitter.label(".Lw32err_enetunreach"); + emitter.instruction("mov eax, 101"); // ENETUNREACH + emitter.instruction("ret"); // return + emitter.label(".Lw32err_econnaborted"); + emitter.instruction("mov eax, 103"); // ECONNABORTED + emitter.instruction("ret"); // return + emitter.label(".Lw32err_enobufs"); + emitter.instruction("mov eax, 105"); // ENOBUFS + emitter.instruction("ret"); // return + emitter.label(".Lw32err_enotconn"); + emitter.instruction("mov eax, 107"); // ENOTCONN + emitter.instruction("ret"); // return + emitter.label(".Lw32err_enospc"); + emitter.instruction("mov eax, 28"); // ENOSPC + emitter.instruction("ret"); // return + emitter.label(".Lw32err_enotempty"); + emitter.instruction("mov eax, 39"); // ENOTEMPTY + emitter.instruction("ret"); // return + emitter.label(".Lw32err_eexist"); + emitter.instruction("mov eax, 17"); // EEXIST + emitter.instruction("ret"); // return + emitter.label(".Lw32err_enametoolong"); + emitter.instruction("mov eax, 36"); // ENAMETOOLONG + emitter.instruction("ret"); // return + emitter.label(".Lw32err_enotdir"); + emitter.instruction("mov eax, 20"); // ENOTDIR + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits the W3f-B msvcrt-real family for `runtime/io/principal_lookup.rs`'s +/// passwd/group lookup: `fopen`/`fgets`/`fclose`/`strncmp`/`strchr`/ +/// `strtoul` all EXIST as standard msvcrt symbols, so each gets a real +/// ABI arg-shuffle shim rather than a stub. `/etc/passwd`/`/etc/group` do +/// not exist on Windows, so `fopen(_etc_passwd_path, "r")` returns `NULL` +/// naturally and the lookup's pre-existing `je ` path handles +/// it — no bespoke "unsupported" behavior is needed for this family. +pub(super) fn emit_shim_msvcrt_passwd_lookup(emitter: &mut Emitter) { + emit_shim_fopen(emitter); + emit_shim_fgets(emitter); + emit_shim_fclose(emitter); + emit_shim_strncmp(emitter); + emit_shim_strchr(emitter); + emit_shim_strtoul(emitter); +} + +/// Emits the `__rt_sys_fopen` shim: converts SysV `fopen(const char* path, +/// const char* mode)` (rdi, rsi) to MSx64 `fopen` (rcx=path, rdx=mode). No +/// register collision, so the two moves may run in either order. Returns a +/// `FILE*` in `rax`; the sole consumer (`principal_lookup.rs:47-48`) does +/// `test rax, rax; je ` — a zero/nonzero pointer test, not a sign +/// test — so no cdqe. +fn emit_shim_fopen(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_fopen"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov rdx, rsi"); // mode → arg2 (rdx) + emitter.instruction("mov rcx, rdi"); // path → arg1 (rcx) + emitter.instruction("call fopen"); // msvcrt fopen (MSx64 ABI, returns FILE* in rax) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return (no cdqe — pointer test only) + emitter.blank(); +} + +/// Emits the `__rt_sys_fgets` shim: converts SysV `fgets(char* buf, int n, +/// FILE* stream)` (rdi, esi, rdx) to MSx64 `fgets` (rcx=buf, edx=n, r8=stream). +/// Register-shuffle hazard: SysV arg3 (stream) is in `rdx`, which is ALSO the +/// MSx64 arg2 (n) target, so it is saved to `r8` BEFORE `rdx` is overwritten +/// by the arg2 shuffle; `edx`←`esi` (n) and `rcx`←`rdi` (buf) move last. +/// Returns a `char*` in `rax`; the sole consumer (`principal_lookup.rs:56-57`) +/// does `test rax, rax; je ` — a zero/nonzero pointer test — so +/// no cdqe. +fn emit_shim_fgets(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_fgets"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov r8, rdx"); // SAVE stream (SysV arg3) before rdx is overwritten + emitter.instruction("mov edx, esi"); // n → arg2 (edx) + emitter.instruction("mov rcx, rdi"); // buf → arg1 (rcx) + emitter.instruction("call fgets"); // msvcrt fgets (MSx64 ABI, returns char* in rax) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return (no cdqe — pointer test only) + emitter.blank(); +} + +/// Emits the `__rt_sys_fclose` shim: converts SysV `fclose(FILE* stream)` +/// (rdi) to MSx64 `fclose` (rcx=stream). Mirrors `emit_shim_zlib_trivial_1arg` +/// (1-arg case). Neither call site (`principal_lookup.rs:79,85`) reads the +/// return value, so no cdqe verdict is reachable. +fn emit_shim_fclose(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_fclose"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov rcx, rdi"); // stream → arg1 (rcx) + emitter.instruction("call fclose"); // msvcrt fclose (MSx64 ABI, returns int in eax) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return (no cdqe — return value unread) + emitter.blank(); +} + +/// Emits the `__rt_sys_strncmp` shim: converts SysV `strncmp(const char* a, +/// const char* b, size_t n)` (rdi, rsi, rdx) to MSx64 `strncmp` (rcx=a, +/// rdx=b, r8=n). Register-shuffle hazard: SysV arg3 (n) is in `rdx`, which is +/// ALSO the MSx64 arg2 (b) target, so it is saved to `r8` BEFORE `rdx` is +/// overwritten by the arg2 shuffle; `rdx`←`rsi` (b) and `rcx`←`rdi` (a) move +/// last. Returns an `int` in `eax`; the sole consumer +/// (`principal_lookup.rs:62-63`) does `test eax, eax; jne ` — a +/// zero/nonzero test, never a sign test — so no cdqe. +fn emit_shim_strncmp(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_strncmp"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov r8, rdx"); // SAVE n (SysV arg3) before rdx is overwritten + emitter.instruction("mov rdx, rsi"); // b → arg2 (rdx) + emitter.instruction("mov rcx, rdi"); // a → arg1 (rcx) + emitter.instruction("call strncmp"); // msvcrt strncmp (MSx64 ABI, returns int in eax) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return (no cdqe — zero/nonzero test only) + emitter.blank(); +} + +/// Emits the `__rt_sys_strchr` shim: converts SysV `strchr(const char* s, +/// int c)` (rdi, esi) to MSx64 `strchr` (rcx=s, edx=c). No register +/// collision, so the two moves may run in either order. Returns a `char*` in +/// `rax`; the sole consumer (`principal_lookup.rs:71-72`) does `test rax, +/// rax; je ` — a zero/nonzero pointer test — so no cdqe. +fn emit_shim_strchr(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_strchr"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov edx, esi"); // c → arg2 (edx) + emitter.instruction("mov rcx, rdi"); // s → arg1 (rcx) + emitter.instruction("call strchr"); // msvcrt strchr (MSx64 ABI, returns char* in rax) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return (no cdqe — pointer test only) + emitter.blank(); +} + +/// Emits the `__rt_sys_strtoul` shim: converts SysV `strtoul(const char* s, +/// char** endptr, int base)` (rdi, rsi, edx) to MSx64 `strtoul` (rcx=s, +/// rdx=endptr, r8d=base). Register-shuffle hazard: SysV arg3 (base) is in +/// `edx`, which is ALSO the MSx64 arg2 (endptr) target, so it is saved to +/// `r8d` BEFORE `rdx` is overwritten by the arg2 shuffle; `rdx`←`rsi` +/// (endptr) and `rcx`←`rdi` (s) move last. +/// +/// No cdqe: the sole consumer (`principal_lookup.rs:76-77`) stores the full +/// `rax` directly with no test at all. `unsigned long` is 32-bit under +/// Windows LLP64 (vs. 64-bit LP64 on Linux/macOS) — but a plain MSx64 write +/// to `eax` (msvcrt `strtoul`'s return register) implicitly zero-extends +/// into `rax` per the x86-64 architecture's register-write rule, and the +/// parsed uid/gid values are always small non-negative numbers, so the +/// zero-extension is exactly the correct widening — no cdqe (sign-extension) +/// would be wrong here even if a consumer did sign-test, since the value is +/// unsigned. +fn emit_shim_strtoul(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_strtoul"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov r8d, edx"); // SAVE base (SysV arg3) before rdx is overwritten + emitter.instruction("mov rdx, rsi"); // endptr → arg2 (rdx) + emitter.instruction("mov rcx, rdi"); // s → arg1 (rcx) + emitter.instruction("call strtoul"); // msvcrt strtoul (returns unsigned long in eax; zero-extends into rax) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return (no cdqe — unsigned, zero-extension is correct) + emitter.blank(); +} diff --git a/src/codegen_support/runtime/win32/shims_compress.rs b/src/codegen_support/runtime/win32/shims_compress.rs new file mode 100644 index 0000000000..52e3263638 --- /dev/null +++ b/src/codegen_support/runtime/win32/shims_compress.rs @@ -0,0 +1,401 @@ +//! Win32 shims for the statically-linked third-party compression/iconv +//! families: zlib (W3d), bzip2 (W3g), and iconv (W3f-A). + +use crate::codegen::emit::Emitter; + +/// Emits the W3d zlib family of `__rt_sys_*` shims (compressBound, deflateEnd, +/// inflateEnd, deflate, inflate, uncompress, inflateInit2_, compress2, +/// deflateInit2_). Unlike the msvcrt/ws2_32/kernel32 shims above, these wrap +/// symbols statically linked from the MinGW-sysroot `libz.a` (via +/// `ELEPHC_MINGW_SYSROOT`, see `src/linker.rs`). That archive is built by +/// MinGW gcc targeting Windows, so it is MSx64 ABI — NOT SysV — exactly like +/// every other Win32-side symbol this module shims; confirmed by +/// disassembling `compress2`/`deflateInit2_`/`deflate`/`inflate`/ +/// `deflateEnd`/`inflateEnd`/`inflateInit2_`/`uncompress` out of a locally +/// cross-built `libzlibstatic.a` (zlib 1.3.1, the same version/build the CI +/// sysroot step produces): every one of them spills its register arguments +/// via `mov %rcx,0x10(%rbp)` / `mov %edx,0x18(%rbp)` / `mov %r8,0x20(%rbp)` / +/// `mov %r9d,0x28(%rbp)` — the standard MSx64 rcx/rdx/r8/r9 prologue, not +/// SysV rdi/rsi/rdx/rcx/r8/r9. +/// +/// Return-value cdqe verdict (Class-3 sign-extension rule): NONE of these +/// nine shims sign-extend `eax`→`rax` after the call. Every runtime consumer +/// of a zlib int-status return tests for EQUALITY, not sign: +/// `uncompress` — `strings.rs` gzuncompress: `test rax,rax; jz ok` (zero = +/// success; a `cdqe`-less negative status zero-extends to a nonzero `rax`, +/// which the zero-test still correctly reports as failure); +/// `inflate` — `strings.rs` gzinflate: `cmp QWORD PTR [.], 1; jne fail` +/// (checks for `Z_STREAM_END`==1, unaffected by upper-bit sign-extension); +/// `deflate`/`deflateEnd`/`inflateEnd`/`inflateInit2_`/`deflateInit2_`/ +/// `compress2` — no site tests their status at all (deflate/inflate read +/// `z_stream.total_out` instead; the Init/End calls are fire-and-forget in +/// every current call site). `compressBound` returns a `uLong` byte count, +/// never negative, so `cdqe` is moot there too. See `emit_shim_zlib_*` below +/// for the per-shim register-shuffle rationale. +pub(super) fn emit_shim_zlib(emitter: &mut Emitter) { + emit_shim_zlib_trivial_1arg(emitter); + emit_shim_zlib_2arg(emitter); + emit_shim_zlib_4arg(emitter); + emit_shim_zlib_compress2(emitter); + emit_shim_zlib_deflate_init2(emitter); +} + +/// Emits the trivial 1-arg zlib shims: `compressBound(srcLen)`, +/// `deflateEnd(strm)`, `inflateEnd(strm)`. SysV: rdi=arg1 → MSx64: rcx=arg1. +/// No cdqe: see [`emit_shim_zlib`] for the per-shim cdqe verdict. +fn emit_shim_zlib_trivial_1arg(emitter: &mut Emitter) { + let shims: &[(&str, &str)] = &[ + ("__rt_sys_compressBound", "compressBound"), + ("__rt_sys_deflateEnd", "deflateEnd"), + ("__rt_sys_inflateEnd", "inflateEnd"), + ]; + for (label, func) in shims { + emitter.label_global(label); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov rcx, rdi"); // arg1 (strm/srcLen) + emitter.instruction(&format!("call {}", func)); // call libz function + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return (no cdqe — see emit_shim_zlib) + emitter.blank(); + } +} + +/// Emits the 2-arg zlib shims: `deflate(strm, flush)`, `inflate(strm, flush)`. +/// SysV: rdi=strm, esi=flush → MSx64: rcx=strm, rdx=flush. No cdqe: see +/// [`emit_shim_zlib`] for the per-shim cdqe verdict. +fn emit_shim_zlib_2arg(emitter: &mut Emitter) { + let shims: &[(&str, &str)] = &[("__rt_sys_deflate", "deflate"), ("__rt_sys_inflate", "inflate")]; + for (label, func) in shims { + emitter.label_global(label); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov rdx, rsi"); // flush → arg2 (rdx) + emitter.instruction("mov rcx, rdi"); // strm → arg1 (rcx) + emitter.instruction(&format!("call {}", func)); // call libz function + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return (no cdqe — see emit_shim_zlib) + emitter.blank(); + } +} + +/// Emits the 4-arg zlib shims: `uncompress(dest, &destLen, source, sourceLen)`, +/// `inflateInit2_(strm, windowBits, version, stream_size)`. SysV: +/// rdi,rsi,rdx,rcx → MSx64: rcx,rdx,r8,r9. Register-shuffle hazard: SysV arg4 +/// is in `rcx`, which is ALSO MSx64 arg1, so it is saved to `r9` BEFORE +/// `rcx` is overwritten; SysV arg3 is in `rdx`, which is ALSO MSx64 arg2, so +/// it is saved to `r8` FIRST (per the `emit_shim_socket_shims` 4-arg idiom). +/// No cdqe: see [`emit_shim_zlib`] for the per-shim cdqe verdict. +fn emit_shim_zlib_4arg(emitter: &mut Emitter) { + let shims: &[(&str, &str)] = &[ + ("__rt_sys_uncompress", "uncompress"), + ("__rt_sys_inflateInit2_", "inflateInit2_"), + ]; + for (label, func) in shims { + emitter.label_global(label); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov r8, rdx"); // SAVE arg3 (SysV rdx) before rdx is overwritten + emitter.instruction("mov r9, rcx"); // SAVE arg4 (SysV rcx) before rcx is overwritten + emitter.instruction("mov rcx, rdi"); // arg1 → rcx + emitter.instruction("mov rdx, rsi"); // arg2 → rdx + emitter.instruction(&format!("call {}", func)); // call libz function + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return (no cdqe — see emit_shim_zlib) + emitter.blank(); + } +} + +/// Emits the `__rt_sys_compress2` shim: converts SysV +/// `compress2(dest, &destLen, source, sourceLen, level)` (rdi, rsi, rdx, rcx, +/// r8) to MSx64 `compress2` (rcx=dest, rdx=&destLen, r8=source, r9=sourceLen, +/// [rsp+32]=level — the 5th arg, on the stack above the 32-byte shadow). +/// +/// Register-shuffle hazard, in the ORDER the shim executes it: SysV arg5 +/// (level) is in `r8`, which is ALSO the MSx64 arg3 target, so it is saved +/// to `r10` and spilled to its stack slot BEFORE `r8` is overwritten by the +/// arg3 shuffle. SysV arg3 (source) is in `rdx`, which is ALSO MSx64 arg2, +/// so it is saved to `r8` (now free) BEFORE `rdx` is overwritten by the arg2 +/// shuffle. SysV arg4 (sourceLen) is in `rcx`, which is ALSO MSx64 arg1, so +/// it is saved to `r9` BEFORE `rcx` is overwritten by the arg1 shuffle. +/// `sub rsp, 56` reserves shadow(32) + the 5th-arg slot(8) + 16 bytes of +/// padding to keep the frame 16-byte aligned (56 ≡ 8 mod 16, matching the +/// mandatory `rsp ≡ 8 (mod 16)` shim-entry convention, so `rsp ≡ 0 (mod 16)` +/// at `call compress2`). No cdqe: see [`emit_shim_zlib`] for the per-shim +/// cdqe verdict — the `test rax,rax; jz` consumer in `gzcompress()` only +/// distinguishes zero from nonzero. +fn emit_shim_zlib_compress2(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_compress2"); + emitter.instruction("sub rsp, 56"); // shadow(32) + 5th-arg slot(8) + pad(16), 16-byte aligned + emitter.instruction("mov r10, r8"); // SAVE arg5 (SysV r8/level) before r8 is overwritten + emitter.instruction("mov QWORD PTR [rsp + 32], r10"); // level → 5th arg (stack slot above shadow) + emitter.instruction("mov r8, rdx"); // SAVE arg3 (SysV rdx/source) before rdx is overwritten + emitter.instruction("mov r9, rcx"); // SAVE arg4 (SysV rcx/sourceLen) before rcx is overwritten + emitter.instruction("mov rcx, rdi"); // dest → arg1 (rcx) + emitter.instruction("mov rdx, rsi"); // &destLen → arg2 (rdx) + emitter.instruction("call compress2"); // zlib compress2 (MSx64 ABI) + emitter.instruction("add rsp, 56"); // restore stack + emitter.instruction("ret"); // return (no cdqe — see emit_shim_zlib) + emitter.blank(); +} + +/// Emits the `__rt_sys_deflateInit2_` shim — the bespoke W3d shim: 8 SysV +/// args (rdi, rsi, edx, ecx, r8d, r9d, plus 2 CALLER-STACK args) to MSx64 (6 +/// register args + 2 stack args). This is the only Class-1 zlib shim with +/// more than 4 arguments in ANY ABI, so both the SysV caller-stack layout +/// AND the MSx64 callee-stack layout matter. +/// +/// SysV caller side (every call site — `strings.rs` gzcompress/gzdeflate +/// and `stream_filters/zlib.rs` — follows this identical pattern): the +/// caller does `sub rsp, 16` then `mov QWORD PTR [rsp+0], version` and +/// `mov QWORD PTR [rsp+8], stream_size` immediately before `call +/// deflateInit2_`. Verified directly against `strings.rs:1129-1133` +/// (`sub rsp, 16` / `[rsp+0]=zlib version address` / `[rsp+8]=Z_STREAM_SIZE` +/// / `call deflateInit2_`) and `stream_filters/zlib.rs:356-360` (identical +/// shape). Since `call` then pushes an 8-byte return address, at shim ENTRY +/// (before this shim allocates its own frame) those two caller-stack args +/// sit at `[rsp+8]` (version) and `[rsp+16]` (stream_size), relative to the +/// shim's entry `rsp` — NOT to any later stack pointer. This is the single +/// most failure-prone offset in the whole family, so both values are read +/// into scratch registers (`rax`, `r10`) BEFORE `sub rsp, 72` shifts what +/// `[rsp+N]` means. +/// +/// MSx64 callee side wants: rcx=strm, rdx=level, r8=method, r9=windowBits, +/// `[rsp+32]`=memLevel, `[rsp+40]`=strategy, `[rsp+48]`=version, +/// `[rsp+56]`=stream_size (4 register args + 4 stack args, the stack args +/// starting immediately above the 32-byte shadow space). Confirmed against a +/// disassembly of `deflateInit2_` cross-built from zlib 1.3.1 for +/// `x86_64-w64-mingw32`: the prologue spills `rcx`→`[rbp+0x10]`, +/// `edx`→`[rbp+0x18]`, `r8d`→`[rbp+0x20]`, `r9d`→`[rbp+0x28]`, then reads its +/// 7th/8th args (version/stream_size) at `[rbp+0x40]`/`[rbp+0x48]` — i.e. +/// caller-stack slots 5–8 sit at `[rsp+32]`/`[rsp+40]`/`[rsp+48]`/`[rsp+56]` +/// from the caller's perspective at `call` time, exactly as this shim lays +/// them out. +/// +/// Register-shuffle order (each SysV register is read into its MSx64 target +/// or a scratch register BEFORE being overwritten by an earlier-numbered +/// MSx64 argument): the two register args furthest from a collision +/// (`r8`→memLevel, `r9`→strategy) are spilled to their stack slots first; +/// then SysV arg4 (`rcx`/windowBits, which collides with MSx64 arg1) is +/// saved to `r9`; SysV arg3 (`rdx`/method, which collides with MSx64 arg2) +/// is saved to `r8`; then `rdx`←`rsi` (level) and finally `rcx`←`rdi` (strm) +/// — `rdi`/`rsi` never collide with an earlier MSx64 write, so they move +/// last. +/// +/// Alignment: shim entry `rsp ≡ 8 (mod 16)` (the universal post-`call` +/// convention every shim in this module assumes). `sub rsp, 72` — shadow +/// (32) + 4 stack-arg slots (32) + 8 bytes of padding — is itself `≡ 8 (mod +/// 16)`, so `rsp` lands exactly on a 16-byte boundary at `call +/// deflateInit2_`. ✅ +/// +/// No cdqe: `deflateInit2_`'s int-status return is never sign-tested by any +/// current call site (`gzcompress`/`gzdeflate`/the zlib stream filter all +/// ignore its return value outright) — see [`emit_shim_zlib`] for the +/// family-wide cdqe verdict. +fn emit_shim_zlib_deflate_init2(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_deflateInit2_"); + emitter.instruction("mov rax, QWORD PTR [rsp + 8]"); // version (caller stack arg7) — read BEFORE sub rsp,72 shifts offsets + emitter.instruction("mov r10, QWORD PTR [rsp + 16]"); // stream_size (caller stack arg8) — read BEFORE sub rsp,72 shifts offsets + emitter.instruction("sub rsp, 72"); // shadow(32) + 4 stack args(32) + pad(8), 16-byte aligned at the call + emitter.instruction("mov QWORD PTR [rsp + 32], r8"); // memLevel (SysV arg5) → MSx64 5th arg (stack) + emitter.instruction("mov QWORD PTR [rsp + 40], r9"); // strategy (SysV arg6) → MSx64 6th arg (stack) + emitter.instruction("mov QWORD PTR [rsp + 48], rax"); // version → MSx64 7th arg (stack) + emitter.instruction("mov QWORD PTR [rsp + 56], r10"); // stream_size → MSx64 8th arg (stack) + emitter.instruction("mov r9, rcx"); // windowBits (SysV arg4) → MSx64 arg4 (r9) BEFORE rcx is overwritten + emitter.instruction("mov r8, rdx"); // method (SysV arg3) → MSx64 arg3 (r8) BEFORE rdx is overwritten + emitter.instruction("mov rdx, rsi"); // level (SysV arg2) → MSx64 arg2 (rdx) + emitter.instruction("mov rcx, rdi"); // strm (SysV arg1) → MSx64 arg1 (rcx) + emitter.instruction("call deflateInit2_"); // zlib deflateInit2_ (MSx64 ABI, 4 reg + 4 stack args) + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return (no cdqe — see emit_shim_zlib) + emitter.blank(); +} + +/// Emits the W3g bzip2 family of `__rt_sys_BZ2_*` shims: `BZ2_bzCompress`, +/// `BZ2_bzCompressInit`, `BZ2_bzCompressEnd` (`stream_filters/bzip2.rs` +/// `emit_compress_x86_64`), and `BZ2_bzBuffToBuffDecompress` +/// (`stream_filters/compress_bzip2_stream.rs` `emit_x86_64`). Like the W3d +/// zlib family (see [`emit_shim_zlib`]), these wrap symbols statically +/// linked from the MinGW-sysroot `libbz2.a` (via `ELEPHC_MINGW_SYSROOT`, +/// `src/linker.rs:406`, `-lbz2`), MSx64 ABI (built by MinGW gcc), NOT SysV. +/// No cdqe on any of the four: every current call site either ignores the +/// libbz2 int-status return outright (`BZ2_bzCompress` in the fwrite loop, +/// `BZ2_bzCompressInit`, `BZ2_bzCompressEnd`) or only equality/zero-tests it +/// (`BZ2_bzCompress` in the close loop: `cmp ..., 4` for `BZ_STREAM_END`; +/// `BZ2_bzBuffToBuffDecompress`: `test eax, eax` / `jnz`) — none sign-tests +/// (`js`/`jl`) the result, so no shim needs a sign-extending `cdqe`. +pub(super) fn emit_shim_bzip2(emitter: &mut Emitter) { + // BZ2_bzCompressEnd(strm) — 1 arg. SysV: rdi=strm → MSx64: rcx=strm. + emitter.label_global("__rt_sys_BZ2_bzCompressEnd"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov rcx, rdi"); // strm → arg1 (rcx) + emitter.instruction("call BZ2_bzCompressEnd"); // libbz2 BZ2_bzCompressEnd (MSx64 ABI) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return (no cdqe — see emit_shim_bzip2) + emitter.blank(); + + // BZ2_bzCompress(strm, action) — 2 args. SysV: rdi=strm, esi=action → + // MSx64: rcx=strm, edx=action. + emitter.label_global("__rt_sys_BZ2_bzCompress"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov edx, esi"); // action → arg2 (edx) + emitter.instruction("mov rcx, rdi"); // strm → arg1 (rcx) + emitter.instruction("call BZ2_bzCompress"); // libbz2 BZ2_bzCompress (MSx64 ABI) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return (no cdqe — see emit_shim_bzip2) + emitter.blank(); + + // BZ2_bzCompressInit(strm, blockSize100k, verbosity, workFactor) — 4 + // args. SysV: rdi=strm, esi=blockSize100k, edx=verbosity, ecx=workFactor + // → MSx64: rcx=strm, rdx=blockSize100k, r8=verbosity, r9=workFactor. + // Register-shuffle hazard (the `emit_shim_zlib_4arg` idiom): SysV arg4 is + // in `rcx`, ALSO the MSx64 arg1 target, so it is saved to `r9` BEFORE + // `rcx` is overwritten; SysV arg3 is in `rdx`, ALSO MSx64 arg2, so it is + // saved to `r8` FIRST. + emitter.label_global("__rt_sys_BZ2_bzCompressInit"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov r8, rdx"); // SAVE arg3 (SysV rdx/verbosity) before rdx is overwritten + emitter.instruction("mov r9, rcx"); // SAVE arg4 (SysV rcx/workFactor) before rcx is overwritten + emitter.instruction("mov rcx, rdi"); // strm → arg1 (rcx) + emitter.instruction("mov rdx, rsi"); // blockSize100k → arg2 (rdx) + emitter.instruction("call BZ2_bzCompressInit"); // libbz2 BZ2_bzCompressInit (MSx64 ABI) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return (no cdqe — see emit_shim_bzip2) + emitter.blank(); + + // BZ2_bzBuffToBuffDecompress(dest, &destLen, source, sourceLen, small, + // verbosity) — 6 args. SysV (regular C ABI, NOT the syscall r10-for-arg4 + // convention `sendto`/`recvfrom` use): rdi=dest, rsi=&destLen, rdx=source, + // ecx=sourceLen, r8d=small, r9d=verbosity (confirmed against the call + // site, `compress_bzip2_stream.rs` `emit_x86_64`@190-196) → MSx64: + // rcx=dest, rdx=&destLen, r8=source, r9d=sourceLen, `[rsp+32]`=small, + // `[rsp+40]`=verbosity (4 reg args + 2 stack args, immediately above the + // 32-byte shadow — the `sendto`/`recvfrom` stack-arg layout, but SysV + // arg4 arrives in `rcx` here, not `r10`). + // + // Register-shuffle order (each SysV register is read BEFORE being + // overwritten by an earlier-numbered MSx64 write): `r9d`/`r8d` + // (verbosity/small) are spilled to their stack slots first (nothing else + // targets them); then SysV arg3 (`rdx`/source, which collides with MSx64 + // arg2) is saved to `r8`; then SysV arg4 (`ecx`/sourceLen, which collides + // with MSx64 arg1) is saved to `r9d`; then `rdx`←`rsi` (&destLen) and + // finally `rcx`←`rdi` (dest) — `rdi`/`rsi` never collide with an earlier + // MSx64 write, so they move last. + // + // `sub rsp, 56` — shadow(32) + 2 stack-arg slots(16) + pad(8) — is `≡ 8 + // (mod 16)`, matching the universal `rsp ≡ 8 (mod 16)` shim-entry + // convention, so `rsp ≡ 0 (mod 16)` at `call BZ2_bzBuffToBuffDecompress`. + emitter.label_global("__rt_sys_BZ2_bzBuffToBuffDecompress"); + emitter.instruction("sub rsp, 56"); // shadow(32) + 2 stack args(16) + pad(8), 16-byte aligned + emitter.instruction("mov DWORD PTR [rsp + 40], r9d"); // verbosity → 6th arg (stack) + emitter.instruction("mov DWORD PTR [rsp + 32], r8d"); // small → 5th arg (stack) + emitter.instruction("mov r8, rdx"); // SAVE arg3 (SysV rdx/source) before rdx is overwritten + emitter.instruction("mov r9d, ecx"); // SAVE arg4 (SysV ecx/sourceLen) before rcx is overwritten + emitter.instruction("mov rdx, rsi"); // &destLen → arg2 (rdx) + emitter.instruction("mov rcx, rdi"); // dest → arg1 (rcx) + emitter.instruction("call BZ2_bzBuffToBuffDecompress"); // libbz2 BZ2_bzBuffToBuffDecompress (MSx64 ABI) + emitter.instruction("add rsp, 56"); // restore stack + emitter.instruction("ret"); // return (no cdqe — see emit_shim_bzip2) + emitter.blank(); +} + +/// Emits the W3f-A `__rt_sys_iconv_open`/`__rt_sys_iconv`/`__rt_sys_iconv_close` +/// family: real ABI shims for `stream_filters/iconv.rs` and +/// `stream_filters/iconv_write.rs`'s x86_64 call sites. Unlike the W3f-B +/// rewrite family below, these are NOT loud-fail stubs: libiconv IS +/// statically linked on windows-x86_64 (`src/linker.rs:256/406`, `-liconv`, +/// MinGW sysroot), MSx64 ABI — the exact same "statically-linked MinGW +/// archive" situation as the W3d zlib family (`emit_shim_zlib`) and the +/// W3e-1 PCRE2-POSIX family (`emit_shim_pcre2_posix`). +/// GNU libiconv exports these entry points with the `libiconv_*` prefix; +/// `` normally supplies the source-level macro aliases, which hand +/// written assembly must apply explicitly. +pub(crate) fn emit_shim_iconv(emitter: &mut Emitter) { + emit_shim_iconv_open(emitter); + emit_shim_iconv_call(emitter); + emit_shim_iconv_close(emitter); +} + +/// Emits the `__rt_sys_iconv_open` shim: converts SysV `iconv_open(const +/// char* tocode, const char* fromcode)` (rdi, rsi) to MSx64 `iconv_open` +/// (rcx=tocode, rdx=fromcode). No register collision (rdi/rsi never overlap +/// rcx/rdx), so the two moves may run in either order. +/// +/// Return-value cdqe verdict: `iconv_open` returns `iconv_t` — a full 64-bit +/// value where failure is `(iconv_t)-1`. `call iconv_open` leaves that +/// already-64-bit value in `rax` untouched by this shim (no truncating +/// 32-bit write occurs anywhere in the shim body), so `(iconv_t)-1` already +/// reads back as all-ones across the full 64-bit `rax` — exactly what the +/// consumer's `cmp rax, -1` / `cmn x0, #1` sign-tests expect. No cdqe needed +/// or possible (cdqe would incorrectly re-sign-extend from a 32-bit `eax` +/// that was never separately written). +fn emit_shim_iconv_open(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_iconv_open"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov rdx, rsi"); // fromcode → arg2 (rdx) + emitter.instruction("mov rcx, rdi"); // tocode → arg1 (rcx) + emitter.instruction("call libiconv_open"); // GNU libiconv open export (MSx64 ABI, returns iconv_t in rax) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return (no cdqe — see doc above) + emitter.blank(); +} + +/// Emits the `__rt_sys_iconv` shim: converts SysV `iconv(iconv_t cd, char** +/// inbuf, size_t* inbytesleft, char** outbuf, size_t* outbytesleft)` (rdi, +/// rsi, rdx, rcx, r8) to MSx64 `iconv` (rcx=cd, rdx=inbuf, r8=inbytesleft, +/// r9=outbuf, `[rsp+32]`=outbytesleft — the 5th arg, on the stack above the +/// 32-byte shadow space). Identical register shape to +/// `emit_shim_pcre2_regexec` (also rdi/rsi/rdx/rcx/r8 → rcx/rdx/r8/r9/stack), +/// so this shim mirrors its register-shuffle order exactly. +/// +/// Register-shuffle order, in the ORDER the shim executes it: SysV arg5 +/// (outbytesleft) is in `r8`, which is ALSO the MSx64 arg3 (inbytesleft) +/// target, so it is saved to `r10` and spilled to its `[rsp+32]` stack slot +/// FIRST, before `r8` is overwritten by the arg3 shuffle. SysV arg3 +/// (inbytesleft) is in `rdx`, which is ALSO MSx64 arg2 (inbuf), so it is +/// saved to `r8` (now free) BEFORE `rdx` is overwritten by the arg2 shuffle. +/// SysV arg4 (outbuf) is in `rcx`, which is ALSO MSx64 arg1 (cd), so it is +/// saved to `r9` BEFORE `rcx` is overwritten by the arg1 shuffle. `rdx`←`rsi` +/// (inbuf) and `rcx`←`rdi` (cd) move last since `rdi`/`rsi` never collide +/// with an earlier MSx64 write. +/// +/// Alignment: shim entry `rsp ≡ 8 (mod 16)`. `sub rsp, 56` reserves shadow +/// (32) + the 5th-arg slot(8) + 16 bytes of padding — `56 ≡ 8 (mod 16)`, so +/// `rsp` lands exactly on a 16-byte boundary at `call iconv`. +/// +/// No cdqe: `iconv` returns a `size_t` conversion count (or `(size_t)-1` on +/// error), but NEITHER x86_64 call site (`stream_filters/iconv.rs`'s read +/// filter, `stream_filters/iconv_write.rs`'s write-filter loop) reads `rax` +/// after the call at all — both recompute the converted/produced byte count +/// from the before/after `outbytesleft` cursor instead (`iconv.rs`: `mov r9, +/// [rsp+24]; sub r9, [rsp+72]`; `iconv_write.rs`: `mov rax, ICONV_SCRATCH; +/// sub rax, [rbp-48]`). The `iconv` return value is write-only dead output +/// at every current call site, so no cdqe verdict is even reachable here. +fn emit_shim_iconv_call(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_iconv"); + emitter.instruction("sub rsp, 56"); // shadow(32) + 5th-arg slot(8) + pad(16), 16-byte aligned + emitter.instruction("mov r10, r8"); // SAVE outbytesleft (SysV arg5/r8) before r8 is overwritten + emitter.instruction("mov QWORD PTR [rsp + 32], r10"); // outbytesleft → MSx64 5th arg (stack slot above shadow) + emitter.instruction("mov r8, rdx"); // SAVE inbytesleft (SysV arg3/rdx) before rdx is overwritten + emitter.instruction("mov r9, rcx"); // SAVE outbuf (SysV arg4/rcx) before rcx is overwritten + emitter.instruction("mov rcx, rdi"); // cd → arg1 (rcx) + emitter.instruction("mov rdx, rsi"); // inbuf → arg2 (rdx) + emitter.instruction("call libiconv"); // GNU libiconv conversion export (MSx64 ABI, returns size_t in rax) + emitter.instruction("add rsp, 56"); // restore stack + emitter.instruction("ret"); // return (no cdqe — see doc above: rax is dead at every call site) + emitter.blank(); +} + +/// Emits the `__rt_sys_iconv_close` shim: converts SysV +/// `iconv_close(iconv_t cd)` (rdi) to MSx64 `iconv_close` (rcx=cd). Mirrors +/// `emit_shim_zlib_trivial_1arg` (1-arg case). `iconv_close` returns an `int` +/// status, but neither x86_64 call site (`iconv.rs`, `iconv_write.rs`) reads +/// `rax` after the call — both immediately move on to unrelated work +/// (`__rt_tmpfile`, reloading the descriptor) — so no cdqe verdict is +/// reachable here either. +fn emit_shim_iconv_close(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_iconv_close"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov rcx, rdi"); // cd → arg1 (rcx) + emitter.instruction("call libiconv_close"); // GNU libiconv close export (MSx64 ABI, returns int in eax) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return (no cdqe — see doc above: rax is dead at every call site) + emitter.blank(); +} diff --git a/src/codegen_support/runtime/win32/shims_encoding.rs b/src/codegen_support/runtime/win32/shims_encoding.rs new file mode 100644 index 0000000000..eefde8bc40 --- /dev/null +++ b/src/codegen_support/runtime/win32/shims_encoding.rs @@ -0,0 +1,118 @@ +//! Purpose: +//! Emits UTF-8/UTF-16 conversion helpers shared by native wide-path Win32 shims. +//! +//! Called from: +//! - `super::emit_win32_shims()` before filesystem shims that call `*W` APIs. +//! +//! Key details: +//! - PHP paths remain UTF-8 internally; Win32 receives allocated UTF-16 strings. +//! - Conversion is strict (`MB_ERR_INVALID_CHARS` / `WC_ERR_INVALID_CHARS`) and includes NUL. +//! - Buffers returned by `__rt_win_utf8_to_utf16` are owned by the caller. + +use crate::codegen::emit::Emitter; + +/// Emits strict UTF-8-to-UTF-16 and UTF-16-to-UTF-8 helpers for Win32 path APIs. +pub(super) fn emit_win32_encoding_helpers(emitter: &mut Emitter) { + emit_utf8_to_utf16(emitter); + emit_utf16_to_utf8(emitter); +} + +/// Emits `__rt_win_utf8_to_utf16(const char*) -> WCHAR*`, allocating the result. +fn emit_utf8_to_utf16(emitter: &mut Emitter) { + emitter.label_global("__rt_win_utf8_to_utf16"); + // -- reject the paths Win32 would silently rewrite, before converting -- + // php-src applies PHP_WIN32_IOUTIL_CHECK_PATH_W at every ioutil entry point + // (win32/ioutil.h): a path ending in a space, or ending in '.' where the + // preceding byte is neither a separator nor another '.', is refused with + // ERROR_ACCESS_DENIED. Win32 otherwise strips those trailing characters, so + // unlink('keep.txt.') would delete 'keep.txt' -- a different file than the one + // named. Every caller of this helper is a path-taking filesystem shim, which is + // exactly the set php-src guards, so the check belongs at this choke point. + emitter.instruction("xor eax, eax"); // scan index for the NUL terminator + emitter.label(".Lpathchk_len"); + emitter.instruction("cmp BYTE PTR [rdi + rax], 0"); // reached the end of the UTF-8 path? + emitter.instruction("je .Lpathchk_have_len"); // rax now holds the byte length + emitter.instruction("inc rax"); // advance one byte + emitter.instruction("jmp .Lpathchk_len"); // keep scanning + emitter.label(".Lpathchk_have_len"); + emitter.instruction("test rax, rax"); // empty path? + emitter.instruction("jz .Lpathchk_ok"); // nothing to reject + emitter.instruction("mov cl, BYTE PTR [rdi + rax - 1]"); // final byte + emitter.instruction("cmp cl, 0x20"); // trailing space is always refused + emitter.instruction("je .Lpathchk_reject"); // -> EACCES + emitter.instruction("cmp cl, 0x2E"); // trailing dot needs the neighbour test + emitter.instruction("jne .Lpathchk_ok"); // any other final byte is fine + emitter.instruction("cmp rax, 1"); // a lone "." names the current directory + emitter.instruction("jbe .Lpathchk_ok"); // and is legal + emitter.instruction("mov cl, BYTE PTR [rdi + rax - 2]"); // byte before the trailing dot + emitter.instruction("cmp cl, 0x2E"); // ".." is legal + emitter.instruction("je .Lpathchk_ok"); // parent directory + emitter.instruction("cmp cl, 0x2F"); // "foo/." is legal + emitter.instruction("je .Lpathchk_ok"); // trailing current-directory component + emitter.instruction("cmp cl, 0x5C"); // "foo\\." likewise, both separators count + emitter.instruction("je .Lpathchk_ok"); // trailing current-directory component + emitter.label(".Lpathchk_reject"); + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], 5"); // ERROR_ACCESS_DENIED, as php sets + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 13"); // EACCES + emitter.instruction("xor eax, eax"); // NULL signals a refused path + emitter.instruction("ret"); // no frame has been established yet + emitter.label(".Lpathchk_ok"); + emitter.instruction("sub rsp, 72"); // shadow space, two stack args, and aligned locals + emitter.instruction("mov QWORD PTR [rsp + 48], rdi"); // preserve the UTF-8 source across Win32 calls + emitter.instruction("mov ecx, 65001"); // CodePage = CP_UTF8 + emitter.instruction("mov edx, 8"); // flags = MB_ERR_INVALID_CHARS + emitter.instruction("mov r8, rdi"); // source UTF-8 string + emitter.instruction("mov r9d, -1"); // include the terminating NUL + emitter.instruction("mov QWORD PTR [rsp + 32], 0"); // query required size without an output buffer + emitter.instruction("mov QWORD PTR [rsp + 40], 0"); // output capacity is zero for the size query + emitter.instruction("call MultiByteToWideChar"); // obtain required WCHAR count + emitter.instruction("test eax, eax"); // did strict UTF-8 validation succeed? + emitter.instruction("jz .Lutf8_to_utf16_fail"); // invalid input or Win32 conversion failure + emitter.instruction("mov DWORD PTR [rsp + 56], eax"); // preserve WCHAR count + emitter.instruction("movsxd rax, eax"); // widen the allocation element count + emitter.instruction("shl rax, 1"); // WCHAR uses two bytes + emitter.instruction("call __rt_heap_alloc"); // allocate the owned UTF-16 result + emitter.instruction("test rax, rax"); // allocation succeeded? + emitter.instruction("jz .Lutf8_to_utf16_fail"); // propagate allocation failure as NULL + emitter.instruction("mov QWORD PTR [rsp + 64], rax"); // preserve the destination pointer + emitter.instruction("mov ecx, 65001"); // CodePage = CP_UTF8 + emitter.instruction("mov edx, 8"); // flags = MB_ERR_INVALID_CHARS + emitter.instruction("mov r8, QWORD PTR [rsp + 48]"); // source UTF-8 string + emitter.instruction("mov r9d, -1"); // include the terminating NUL + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // destination UTF-16 buffer + emitter.instruction("mov eax, DWORD PTR [rsp + 56]"); // reload WCHAR capacity + emitter.instruction("mov QWORD PTR [rsp + 40], rax"); // destination capacity in WCHARs + emitter.instruction("call MultiByteToWideChar"); // perform strict conversion + emitter.instruction("test eax, eax"); // conversion succeeded? + emitter.instruction("jz .Lutf8_to_utf16_free_fail"); // release the allocation on failure + emitter.instruction("mov rax, QWORD PTR [rsp + 64]"); // return the owned UTF-16 buffer + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return WCHAR pointer + emitter.label(".Lutf8_to_utf16_free_fail"); + emitter.instruction("mov rax, QWORD PTR [rsp + 64]"); // allocation to release + emitter.instruction("call __rt_heap_free"); // avoid leaking a failed conversion buffer + emitter.label(".Lutf8_to_utf16_fail"); + emitter.instruction("xor eax, eax"); // NULL signals conversion failure + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return NULL + emitter.blank(); +} + +/// Emits `__rt_win_utf16_to_utf8(const WCHAR*, char*, int) -> int`. +fn emit_utf16_to_utf8(emitter: &mut Emitter) { + emitter.label_global("__rt_win_utf16_to_utf8"); + emitter.instruction("sub rsp, 72"); // shadow space and four stack args, aligned + emitter.instruction("mov QWORD PTR [rsp + 32], rsi"); // destination buffer is Win32 argument five + emitter.instruction("mov QWORD PTR [rsp + 40], rdx"); // destination byte capacity is argument six + emitter.instruction("mov QWORD PTR [rsp + 48], 0"); // no replacement character under strict conversion + emitter.instruction("mov QWORD PTR [rsp + 56], 0"); // caller does not need the used-default-char flag + emitter.instruction("mov ecx, 65001"); // CodePage = CP_UTF8 + emitter.instruction("mov edx, 128"); // flags = WC_ERR_INVALID_CHARS + emitter.instruction("mov r8, rdi"); // source UTF-16 string + emitter.instruction("mov r9d, -1"); // include the terminating UTF-16 NUL + emitter.instruction("call WideCharToMultiByte"); // convert the entry name to PHP's UTF-8 representation + emitter.instruction("cdqe"); // return the signed Win32 byte count + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return bytes including NUL, or zero on failure + emitter.blank(); +} diff --git a/src/codegen_support/runtime/win32/shims_errors.rs b/src/codegen_support/runtime/win32/shims_errors.rs new file mode 100644 index 0000000000..dbe35f36d6 --- /dev/null +++ b/src/codegen_support/runtime/win32/shims_errors.rs @@ -0,0 +1,89 @@ +//! Purpose: +//! Emits Windows error-message formatting helpers for PHP-visible diagnostics. +//! +//! Called from: +//! - `super::emit_win32_shims()` after the UTF conversion helpers. +//! +//! Key details: +//! - Native messages come from `FormatMessageW`, never from the active ANSI code page. +//! - The returned UTF-8 buffer is heap-owned and must be released by its consumer. +//! - The helper trims neither punctuation nor whitespace; callers own PHP-specific presentation. + +use crate::codegen::emit::Emitter; + +/// Emits `__rt_win32_error_message(code)`, returning an owned UTF-8 system message or NULL. +pub(super) fn emit_win32_error_message(emitter: &mut Emitter) { + emitter.label_global("__rt_win32_error_message"); + emitter.instruction("sub rsp, 1112"); // shadow, FormatMessageW stack args, 512 WCHARs, and aligned locals + emitter.instruction("mov ecx, 0x1200"); // FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS + emitter.instruction("xor edx, edx"); // no message source module + emitter.instruction("mov r8d, edi"); // native Win32 or Winsock message identifier + emitter.instruction("xor r9d, r9d"); // use the caller's default UI language + emitter.instruction("lea rax, [rsp + 64]"); // stack-backed 512-WCHAR message buffer + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // lpBuffer + emitter.instruction("mov QWORD PTR [rsp + 40], 512"); // nSize in WCHARs + emitter.instruction("mov QWORD PTR [rsp + 48], 0"); // no insert arguments + emitter.instruction("call FormatMessageW"); // obtain the localized Unicode system message + emitter.instruction("test eax, eax"); // did Windows know this message identifier? + emitter.instruction("jz .Lwin_error_message_fail"); // unknown code or formatting failure + // -- query the exact UTF-8 allocation size, including the terminator -- + emitter.instruction("mov ecx, 65001"); // CodePage = CP_UTF8 + emitter.instruction("xor edx, edx"); // valid system UTF-16 needs no special flags + emitter.instruction("lea r8, [rsp + 64]"); // formatted UTF-16 message + emitter.instruction("mov r9d, -1"); // include terminating NUL + emitter.instruction("mov QWORD PTR [rsp + 32], 0"); // size query has no destination + emitter.instruction("mov QWORD PTR [rsp + 40], 0"); // destination capacity zero requests required bytes + emitter.instruction("mov QWORD PTR [rsp + 48], 0"); // no replacement character + emitter.instruction("mov QWORD PTR [rsp + 56], 0"); // caller does not need used-default-char state + emitter.instruction("call WideCharToMultiByte"); // obtain required UTF-8 byte count + emitter.instruction("test eax, eax"); // conversion size query succeeded? + emitter.instruction("jz .Lwin_error_message_fail"); // propagate conversion failure + emitter.instruction("mov DWORD PTR [rsp + 1092], eax"); // preserve byte capacity + emitter.instruction("cdqe"); // widen allocation size + emitter.instruction("call __rt_heap_alloc"); // allocate owned UTF-8 message + emitter.instruction("test rax, rax"); // allocation succeeded? + emitter.instruction("jz .Lwin_error_message_fail"); // return NULL on allocation failure + emitter.instruction("mov QWORD PTR [rsp + 1096], rax"); // preserve output pointer + emitter.instruction("lea rdi, [rsp + 64]"); // helper arg1 = UTF-16 message + emitter.instruction("mov rsi, rax"); // helper arg2 = allocated UTF-8 destination + emitter.instruction("mov edx, DWORD PTR [rsp + 1092]"); // helper arg3 = byte capacity + emitter.instruction("call __rt_win_utf16_to_utf8"); // perform strict UTF-8 conversion + emitter.instruction("test eax, eax"); // conversion succeeded? + emitter.instruction("jz .Lwin_error_message_free_fail"); // release allocation on failure + emitter.instruction("mov rax, QWORD PTR [rsp + 1096]"); // return owned UTF-8 message + emitter.instruction("add rsp, 1112"); // restore stack + emitter.instruction("ret"); // return message pointer + emitter.label(".Lwin_error_message_free_fail"); + emitter.instruction("mov rax, QWORD PTR [rsp + 1096]"); // failed output allocation + emitter.instruction("call __rt_heap_free"); // avoid leaking a partially converted message + emitter.label(".Lwin_error_message_fail"); + emitter.instruction("xor eax, eax"); // NULL means no system message was available + emitter.instruction("add rsp, 1112"); // restore stack + emitter.instruction("ret"); // return NULL + emitter.blank(); +} + +/// Emits failure-only capture helpers for Win32 and Winsock shims. +pub(super) fn emit_native_errno_capture_helpers(emitter: &mut Emitter) { + emitter.label_global("__rt_win32_capture_errno"); + emitter.instruction("sub rsp, 40"); // align and reserve Win32 shadow space + emitter.instruction("call GetLastError"); // fetch the failing kernel32 operation's native code + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // retain native Win32 state separately + emitter.instruction("call __rt_win32_errno_from_code"); // translate to the runtime's POSIX errno space + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish errno for observable failure handling + emitter.instruction("mov rax, -1"); // standard POSIX failure sentinel + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return -1 + emitter.blank(); + + emitter.label_global("__rt_wsa_capture_errno"); + emitter.instruction("sub rsp, 40"); // align and reserve Winsock shadow space + emitter.instruction("call WSAGetLastError"); // fetch the failing Winsock operation's native code + emitter.instruction("mov DWORD PTR [rip + __rt_wsa_last_error], eax"); // retain Winsock state separately from Win32 state + emitter.instruction("call __rt_win32_errno_from_code"); // translate WSA error to POSIX errno + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish errno for PHP stream/socket code + emitter.instruction("mov rax, -1"); // standard SOCKET_ERROR/POSIX failure sentinel + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return -1 + emitter.blank(); +} diff --git a/src/codegen_support/runtime/win32/shims_fs.rs b/src/codegen_support/runtime/win32/shims_fs.rs new file mode 100644 index 0000000000..11cede3f7f --- /dev/null +++ b/src/codegen_support/runtime/win32/shims_fs.rs @@ -0,0 +1,2323 @@ +//! Win32 shims for the filesystem/fd family: open/read/write/close/seek/ +//! stat/rename/chmod/access/mkdir/rmdir/getcwd/chdir/statfs/getdents, the +//! temp-dir/dir-rewrite-stub helpers, and the php-parity Windows ACL check +//! (`GetFileSecurityW` + `AccessCheck`) that backs `is_readable()`/ +//! `is_writable()`. + +use crate::codegen::emit::Emitter; +use crate::codegen_support::sentinels; + +/// Emits a shim that converts SysV `write(fd, buf, len)` to Win32 `WriteFile`. +/// +/// SysV: rdi=fd, rsi=buf, rdx=len → MSx64: rcx=handle, rdx=buf, r8=len, r9=&written +/// Native failures are translated into the shared POSIX errno slot and return +/// -1; the bytes-written out parameter is consumed only after `WriteFile` +/// reports success. +/// +/// Descriptors opened with `O_APPEND` are repositioned to end-of-file first. +/// `__rt_sys_open` records the Linux open flags in the per-descriptor status +/// cache and sets `_O_APPEND` on the CRT descriptor, but neither has any effect +/// here: `WriteFile` writes at the handle's own file pointer, which `OPEN_ALWAYS` +/// leaves at zero, and the CRT's append seek only runs inside `_write`, which +/// this shim deliberately bypasses. Without the seek, `fopen($f, 'a')` silently +/// overwrites the file from byte 0. php-src reaches the same end state through +/// the CRT (main/streams/plain_wrapper.c php_stdiop_write calls `_write`, and +/// win32/ioutil.c hands `_open_osfhandle` the `_O_APPEND` flag); the seek is +/// performed explicitly here because the write path is native. +pub(super) fn emit_shim_write(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_write"); + emitter.instruction("sub rsp, 72"); // allocate shadow(32) + written(4) + len/buf/fd spill slots + emitter.instruction("mov QWORD PTR [rsp + 48], rdx"); // spill len (rdx is volatile, clobbered by the call) to a safe slot + emitter.instruction("mov QWORD PTR [rsp + 56], rsi"); // spill the buffer across the status lookup, which clobbers rsi + emitter.instruction("mov QWORD PTR [rsp + 64], rdi"); // retain the CRT descriptor for the cached-status lookup + emitter.instruction("mov rcx, rdi"); // fd for handle conversion + emitter.instruction("call __rt_fd_to_handle"); // convert fd to Win32 HANDLE + emitter.instruction("cmp rax, -1"); // not a CRT file descriptor (socket resource)? + emitter.instruction("je .Lsys_write_socket"); // Winsock streams use send(), not WriteFile + emitter.instruction("mov QWORD PTR [rsp + 40], rax"); // park the native HANDLE while the status cache is consulted + emitter.instruction("mov rdi, QWORD PTR [rsp + 64]"); // cached status is keyed by the CRT descriptor + emitter.instruction("call __rt_win_fd_status_find"); // inspect the recorded Linux open flags + emitter.instruction("test rax, rax"); // did this descriptor receive explicit status flags? + emitter.instruction("jz .Lsys_write_file"); // no cache entry: write at the current file pointer + emitter.instruction("test QWORD PTR [rax + 16], 0x400"); // is O_APPEND set in the cached Linux status flags? + emitter.instruction("jz .Lsys_write_file"); // ordinary descriptors keep their file pointer + emitter.instruction("mov rcx, QWORD PTR [rsp + 40]"); // hFile = the parked native HANDLE + emitter.instruction("xor edx, edx"); // liDistanceToMove = 0 + emitter.instruction("xor r8d, r8d"); // lpNewFilePointer = NULL + emitter.instruction("mov r9d, 2"); // dwMoveMethod = FILE_END + emitter.instruction("call SetFilePointerEx"); // reposition to end-of-file so the write appends + emitter.label(".Lsys_write_file"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 40]"); // handle + emitter.instruction("mov rdx, QWORD PTR [rsp + 56]"); // buffer + emitter.instruction("mov r8, QWORD PTR [rsp + 48]"); // reload len (arg3) after the handle-conversion call + emitter.instruction("mov QWORD PTR [rsp + 32], 0"); // lpOverlapped = NULL (arg5 slot) + emitter.instruction("lea r9, [rsp + 40]"); // &bytesWritten (arg4 -> [rsp+40]) + emitter.instruction("call WriteFile"); // WriteFile(handle, buf, len, &written, NULL) + emitter.instruction("test eax, eax"); // BOOL result: zero means the write failed + emitter.instruction("jz .Lsys_write_file_fail"); // translate the native error instead of reading an undefined out-param + emitter.instruction("mov eax, DWORD PTR [rsp + 40]"); // success: return the DWORD byte count + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return to caller + emitter.label(".Lsys_write_file_fail"); + emitter.instruction("call GetLastError"); // obtain the failed WriteFile status + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // preserve the native diagnostic independently of errno + emitter.instruction("call __rt_win32_errno_from_code"); // translate the Win32 failure to POSIX errno + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish errno for fwrite and direct write callers + emitter.instruction("mov rax, -1"); // POSIX write failure sentinel + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return -1 to the caller + emitter.label(".Lsys_write_socket"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 64]"); // SOCKET + emitter.instruction("mov rdx, QWORD PTR [rsp + 56]"); // buffer + emitter.instruction("mov r8, QWORD PTR [rsp + 48]"); // byte count + emitter.instruction("xor r9d, r9d"); // flags = 0 + emitter.instruction("call send"); // socket-stream write + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je .Lsys_write_socket_fail"); // publish Winsock errno + emitter.instruction("cdqe"); // signed byte count + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return byte count + emitter.label(".Lsys_write_socket_fail"); + emitter.instruction("call __rt_wsa_capture_errno"); // translate WSAGetLastError + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return -1 + emitter.blank(); +} + +/// Emits a shim that converts SysV `read(fd, buf, len)` to Win32 `ReadFile`. +/// +/// On failure (`ReadFile` returns `FALSE`), translates `GetLastError()` to a +/// POSIX errno via `__rt_win32_errno_from_code` (`shims_c_symbols.rs`), +/// stores it into `__rt_errno`, and returns -1 like POSIX `read()` — this is +/// what lets the `fgets`/`fread` EAGAIN-vs-EOF check (`__errno_location`, +/// compared against 11) tell a nonblocking would-block apart from a real +/// EOF/error. CRT descriptors marked `O_NONBLOCK` in the status cache first +/// probe named-pipe availability with `PeekNamedPipe`. PHP's Windows plain +/// wrapper polls a default nonblocking process pipe briefly before reporting +/// an empty read, so this shim waits in bounded one-millisecond intervals for +/// child output instead of racing the process start. The final `ReadFile` +/// request is capped to the availability result just as php-src caps `read()`. +/// On success errno is left untouched (POSIX does not clear it). +pub(super) fn emit_shim_read(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_read"); + emitter.instruction("sub rsp, 104"); // allocate shadow space, API out-params, arguments, and a pipe poll counter + emitter.instruction("mov QWORD PTR [rsp + 72], rdx"); // spill len (rdx is volatile, clobbered by the call) to a safe slot + emitter.instruction("mov QWORD PTR [rsp + 80], rdi"); // retain the CRT descriptor for cached-status lookup + emitter.instruction("mov QWORD PTR [rsp + 88], rsi"); // retain the destination buffer across status and Win32 calls + emitter.instruction("test rdx, rdx"); // POSIX read(fd, buf, 0) must not inspect pipe readiness + emitter.instruction("jnz .Lsys_read_nonempty"); // nonzero requests may need native I/O + emitter.instruction("xor eax, eax"); // zero-length reads succeed immediately, including O_NONBLOCK pipes + emitter.instruction("add rsp, 104"); // restore the caller stack before the fast return + emitter.instruction("ret"); // return zero without calling PeekNamedPipe or ReadFile + emitter.label(".Lsys_read_nonempty"); + emitter.instruction("mov rcx, rdi"); // fd for handle conversion + emitter.instruction("call __rt_fd_to_handle"); // convert fd to Win32 HANDLE + emitter.instruction("cmp rax, -1"); // not a CRT file descriptor (socket resource)? + emitter.instruction("je .Lsys_read_socket"); // Winsock streams use recv(), not ReadFile + emitter.instruction("mov QWORD PTR [rsp + 64], rax"); // preserve the native HANDLE across the status lookup + emitter.instruction("mov rdi, QWORD PTR [rsp + 80]"); // cached status is keyed by the CRT descriptor + emitter.instruction("call __rt_win_fd_status_find"); // inspect F_GETFL-visible flags without changing ownership + emitter.instruction("test rax, rax"); // did this descriptor receive explicit status flags? + emitter.instruction("jz .Lsys_read_file"); // no cache entry: retain normal blocking ReadFile behavior + emitter.instruction("test QWORD PTR [rax + 16], 0x800"); // is O_NONBLOCK set in the cached Linux status flags? + emitter.instruction("jz .Lsys_read_file"); // blocking descriptors may proceed directly to ReadFile + // -- Windows anonymous pipes do not support overlapped I/O: poll like php-src before ReadFile -- + emitter.instruction("mov QWORD PTR [rsp + 96], 0"); // no nonblocking pipe polls have elapsed yet + emitter.label(".Lsys_read_nonblocking_pipe_probe"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 64]"); // hNamedPipe = converted parent pipe HANDLE + emitter.instruction("xor edx, edx"); // lpBuffer = NULL (availability-only probe) + emitter.instruction("xor r8d, r8d"); // nBufferSize = 0 + emitter.instruction("xor r9d, r9d"); // lpBytesRead = NULL + emitter.instruction("lea rax, [rsp + 56]"); // address of total available-byte out-param + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // lpTotalBytesAvail (arg5) + emitter.instruction("mov QWORD PTR [rsp + 40], 0"); // lpBytesLeftThisMessage = NULL (arg6) + emitter.instruction("call PeekNamedPipe"); // probe anonymous-pipe data without waiting + emitter.instruction("test eax, eax"); // is this a readable named/anonymous pipe? + emitter.instruction("jz .Lsys_read_file"); // non-pipe/error falls back to ReadFile's existing error behavior + emitter.instruction("cmp DWORD PTR [rsp + 56], 0"); // did the probe report immediately readable bytes? + emitter.instruction("jne .Lsys_read_nonblocking_pipe_ready"); // cap the read to ready bytes before entering synchronous ReadFile + emitter.instruction("inc QWORD PTR [rsp + 96]"); // account for this empty nonblocking pipe probe + emitter.instruction("cmp QWORD PTR [rsp + 96], 32000"); // preserve php-src's roughly 32-second bounded wait + emitter.instruction("jae .Lsys_read_nonblocking_pipe_ready"); // timeout issues PHP's zero-length read instead of blocking forever + emitter.instruction("mov ecx, 1"); // sleep one millisecond before polling child output again + emitter.instruction("call Sleep"); // yield while a Windows child initializes or flushes + emitter.instruction("jmp .Lsys_read_nonblocking_pipe_probe"); // repeat until bytes, EOF, a non-pipe failure, or timeout + emitter.label(".Lsys_read_nonblocking_pipe_ready"); + emitter.instruction("mov eax, DWORD PTR [rsp + 56]"); // load PeekNamedPipe's currently available byte count + emitter.instruction("mov rdx, QWORD PTR [rsp + 72]"); // reload the caller's requested byte count + emitter.instruction("cmp rdx, rax"); // is the PHP read request larger than the ready data? + emitter.instruction("cmova rdx, rax"); // never let synchronous ReadFile wait for unavailable bytes + emitter.instruction("mov QWORD PTR [rsp + 72], rdx"); // carry the capped request into the shared ReadFile path + emitter.label(".Lsys_read_file"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 64]"); // restore HANDLE after optional availability probe + emitter.instruction("mov rdx, QWORD PTR [rsp + 88]"); // restore caller destination buffer + emitter.instruction("mov r8, QWORD PTR [rsp + 72]"); // reload len (arg3) after helper calls + emitter.instruction("mov QWORD PTR [rsp + 32], 0"); // lpOverlapped = NULL (arg5 slot) + emitter.instruction("lea r9, [rsp + 48]"); // &bytesRead (arg4) + emitter.instruction("call ReadFile"); // ReadFile(handle, buf, len, &read, NULL) + emitter.instruction("test eax, eax"); // BOOL result: 0 means the ReadFile call failed + emitter.instruction("jz .Lsys_read_fail"); // failure: translate GetLastError and return -1 + emitter.instruction("mov eax, DWORD PTR [rsp + 48]"); // success: return bytes read (DWORD out-param; zero-extend) + emitter.instruction("add rsp, 104"); // restore stack + emitter.instruction("ret"); // return to caller + emitter.label(".Lsys_read_socket"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 80]"); // SOCKET + emitter.instruction("mov rdx, QWORD PTR [rsp + 88]"); // destination buffer + emitter.instruction("mov r8, QWORD PTR [rsp + 72]"); // byte count + emitter.instruction("xor r9d, r9d"); // flags = 0 + emitter.instruction("call recv"); // socket-stream read + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je .Lsys_read_socket_fail"); // publish Winsock errno + emitter.instruction("cdqe"); // signed byte count + emitter.instruction("add rsp, 104"); // restore stack + emitter.instruction("ret"); // return byte count + emitter.label(".Lsys_read_socket_fail"); + emitter.instruction("call __rt_wsa_capture_errno"); // translate WSAGetLastError + emitter.instruction("add rsp, 104"); // restore stack + emitter.instruction("ret"); // return -1 + emitter.label(".Lsys_read_fail"); + emitter.instruction("call GetLastError"); // fetch the Win32 code left by the failed ReadFile + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // preserve the native Win32 error separately from errno + emitter.instruction("call __rt_win32_errno_from_code"); // translate Win32 code (eax) to POSIX errno (eax) + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish errno for __errno_location readers + emitter.instruction("mov rax, -1"); // ReadFile failure: return -1 like POSIX read() + emitter.instruction("add rsp, 104"); // restore stack + emitter.instruction("ret"); // return to caller + emitter.blank(); +} + +/// Emits the bounded opaque-handle-to-stream-slot registry used by Windows I/O state. +/// +/// `SOCKET` is a pointer-sized opaque value and can exceed every fixed runtime +/// table bound. The lookup returns a stable slot in `0..256` for each live +/// stream handle; a full registry deliberately falls back to slot zero so no +/// generated path can turn a native handle into an out-of-bounds address. +pub(super) fn emit_win_stream_slot_registry(emitter: &mut Emitter) { + emitter.label_global("__rt_win_stream_slot"); + emitter.instruction("mov r8, rdi"); // retain the opaque descriptor while scanning registry state + crate::codegen_support::abi::emit_symbol_address(emitter, "r9", "_win_stream_slot_handles"); // handle table base + crate::codegen_support::abi::emit_symbol_address(emitter, "r10", "_win_stream_slot_used"); // occupancy table base + emitter.instruction("xor ecx, ecx"); // scan slot index = 0 + emitter.instruction("mov rdx, -1"); // first-free slot sentinel + emitter.label(".Lwin_stream_slot_scan"); + emitter.instruction("cmp ecx, 256"); // exhausted all bounded slots? + emitter.instruction("je .Lwin_stream_slot_allocate"); // allocate the remembered free slot or use fallback + emitter.instruction("cmp BYTE PTR [r10 + rcx], 0"); // occupied slot? + emitter.instruction("je .Lwin_stream_slot_free"); // remember the first available slot + emitter.instruction("cmp QWORD PTR [r9 + rcx * 8], r8"); // does this slot belong to the requested opaque handle? + emitter.instruction("je .Lwin_stream_slot_found"); // return the existing stable slot + emitter.instruction("inc rcx"); // inspect the next entry + emitter.instruction("jmp .Lwin_stream_slot_scan"); // continue registry lookup + emitter.label(".Lwin_stream_slot_free"); + emitter.instruction("cmp rdx, -1"); // already remembered a free slot? + emitter.instruction("jne .Lwin_stream_slot_next"); // retain the earliest free slot for deterministic reuse + emitter.instruction("mov rdx, rcx"); // remember this free slot + emitter.label(".Lwin_stream_slot_next"); + emitter.instruction("inc rcx"); // inspect remaining entries for an existing mapping + emitter.instruction("jmp .Lwin_stream_slot_scan"); // continue registry lookup + emitter.label(".Lwin_stream_slot_found"); + emitter.instruction("mov rax, rcx"); // return the existing opaque-handle slot + emitter.instruction("ret"); // return to the stream-state caller + emitter.label(".Lwin_stream_slot_allocate"); + emitter.instruction("cmp rdx, -1"); // a free slot was found? + emitter.instruction("je .Lwin_stream_slot_full"); // bounded registry is saturated + emitter.instruction("mov QWORD PTR [r9 + rdx * 8], r8"); // bind the requested handle to the free slot + emitter.instruction("mov BYTE PTR [r10 + rdx], 1"); // mark the slot live before any table consumer observes it + emitter.instruction("mov rax, rdx"); // return the newly allocated stable slot + emitter.instruction("ret"); // return to the stream-state caller + emitter.label(".Lwin_stream_slot_full"); + emitter.instruction("xor eax, eax"); // safe bounded fallback avoids opaque-handle table indexing + emitter.instruction("ret"); // return fallback slot zero + emitter.blank(); +} + +/// Emits release of an opaque Windows stream slot when its descriptor closes. +/// +/// The registry owns no OS handle; this helper only clears the handle mapping +/// and all state keyed by its compact slot so a recycled SOCKET cannot inherit +/// EOF or filter state from its predecessor. +pub(super) fn emit_win_stream_slot_clear(emitter: &mut Emitter) { + emitter.label_global("__rt_win_stream_slot_clear"); + emitter.instruction("mov r8, rdi"); // preserve the closing opaque descriptor while scanning + crate::codegen_support::abi::emit_symbol_address(emitter, "r9", "_win_stream_slot_handles"); // handle table base + crate::codegen_support::abi::emit_symbol_address(emitter, "r10", "_win_stream_slot_used"); // occupancy table base + emitter.instruction("xor ecx, ecx"); // begin at slot zero + emitter.label(".Lwin_stream_slot_clear_scan"); + emitter.instruction("cmp ecx, 256"); // mapping was absent? + emitter.instruction("je .Lwin_stream_slot_clear_done"); // nothing to release + emitter.instruction("cmp BYTE PTR [r10 + rcx], 0"); // live registry entry? + emitter.instruction("je .Lwin_stream_slot_clear_next"); // skip unused entries + emitter.instruction("cmp QWORD PTR [r9 + rcx * 8], r8"); // entry belongs to this descriptor? + emitter.instruction("je .Lwin_stream_slot_clear_found"); // clear its compact stream state + emitter.label(".Lwin_stream_slot_clear_next"); + emitter.instruction("inc rcx"); // inspect the next entry + emitter.instruction("jmp .Lwin_stream_slot_clear_scan"); // continue bounded lookup + emitter.label(".Lwin_stream_slot_clear_found"); + emitter.instruction("mov BYTE PTR [r10 + rcx], 0"); // release occupancy before descriptor reuse + emitter.instruction("mov QWORD PTR [r9 + rcx * 8], 0"); // discard stale opaque handle value + crate::codegen_support::abi::emit_symbol_address(emitter, "r11", "_eof_flags"); // EOF state table base + emitter.instruction("mov BYTE PTR [r11 + rcx], 0"); // clear inherited EOF state + crate::codegen_support::abi::emit_symbol_address(emitter, "r11", "_win_stream_timed_out"); // timeout state table base + emitter.instruction("mov BYTE PTR [r11 + rcx], 0"); // clear inherited timeout state + crate::codegen_support::abi::emit_symbol_address(emitter, "r11", "_stream_read_filters"); // read filter table base + emitter.instruction("mov BYTE PTR [r11 + rcx], 0"); // clear inherited read filter state + crate::codegen_support::abi::emit_symbol_address(emitter, "r11", "_stream_write_filters"); // write filter table base + emitter.instruction("mov BYTE PTR [r11 + rcx], 0"); // clear inherited write filter state + emitter.label(".Lwin_stream_slot_clear_done"); + emitter.instruction("ret"); // return without closing the descriptor itself + emitter.blank(); +} + +/// Emits a bounded timeout-state update for the Windows stream descriptor in `rdi`. +pub(super) fn emit_win_stream_mark_timed_out(emitter: &mut Emitter) { + emitter.label_global("__rt_win_stream_mark_timed_out"); + emitter.instruction("call __rt_win_stream_slot"); // resolve the opaque descriptor to a bounded stream-state slot + crate::codegen_support::abi::emit_symbol_address(emitter, "r10", "_win_stream_timed_out"); // timeout state table base + emitter.instruction("mov BYTE PTR [r10 + rax], 1"); // expose the most recent ETIMEDOUT through stream metadata + emitter.instruction("ret"); // return after recording the retryable timeout state + emitter.blank(); +} + +/// Emits a bounded timeout-state reset for the Windows stream descriptor in `rdi`. +pub(super) fn emit_win_stream_clear_timed_out(emitter: &mut Emitter) { + emitter.label_global("__rt_win_stream_clear_timed_out"); + emitter.instruction("call __rt_win_stream_slot"); // resolve the opaque descriptor to a bounded stream-state slot + crate::codegen_support::abi::emit_symbol_address(emitter, "r10", "_win_stream_timed_out"); // timeout state table base + emitter.instruction("mov BYTE PTR [r10 + rax], 0"); // a new stream operation clears the previous timeout outcome + emitter.instruction("ret"); // return after resetting timeout metadata + emitter.blank(); +} + +/// Emits a shim that closes either a CRT file descriptor or a raw Winsock +/// SOCKET resource published by Windows `proc_open(["socket"])`. +pub(super) fn emit_shim_close(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_close"); + emitter.instruction("sub rsp, 56"); // shadow space and saved opaque descriptor + emitter.instruction("mov QWORD PTR [rsp + 48], rdi"); // retain descriptor across CRT probing + emitter.instruction("mov ecx, edi"); // candidate CRT descriptor + emitter.instruction("call _get_osfhandle"); // raw SOCKET values are not in the CRT fd table + emitter.instruction("cmp rax, -1"); // did CRT recognize the descriptor? + emitter.instruction("je .Lsys_close_socket"); // raw SOCKET must use closesocket + emitter.instruction("mov rdi, QWORD PTR [rsp + 48]"); // restore CRT descriptor for metadata release + emitter.instruction("call __rt_win_fd_status_clear"); // discard cached fcntl flags before descriptor reuse + emitter.instruction("mov rdi, QWORD PTR [rsp + 48]"); // restore descriptor for compact stream-state release + emitter.instruction("call __rt_user_filter_release_fd"); // run user-filter onClose hooks while the opaque-handle mapping is live + emitter.instruction("mov rdi, QWORD PTR [rsp + 48]"); // restore descriptor after user-filter lifecycle callbacks + emitter.instruction("call __rt_win_stream_slot_clear"); // release bounded EOF/filter slot before CRT descriptor reuse + emitter.instruction("mov ecx, DWORD PTR [rsp + 48]"); // CRT file descriptor + emitter.instruction("call _close"); // close descriptor and its underlying HANDLE + emitter.instruction("cdqe"); // preserve msvcrt's signed close result + emitter.instruction("add rsp, 56"); // restore stack + emitter.instruction("ret"); // return CRT close status + emitter.label(".Lsys_close_socket"); + emitter.instruction("mov rdi, QWORD PTR [rsp + 48]"); // raw SOCKET metadata key + emitter.instruction("call __rt_win_fd_status_clear"); // discard any cached socket status without table indexing + emitter.instruction("mov rdi, QWORD PTR [rsp + 48]"); // restore raw SOCKET for compact stream-state release + emitter.instruction("call __rt_user_filter_release_fd"); // run user-filter onClose hooks while the opaque-handle mapping is live + emitter.instruction("mov rdi, QWORD PTR [rsp + 48]"); // restore raw SOCKET after user-filter lifecycle callbacks + emitter.instruction("call __rt_win_stream_slot_clear"); // release bounded EOF/filter slot before SOCKET reuse + emitter.instruction("mov rcx, QWORD PTR [rsp + 48]"); // SOCKET argument for Winsock + emitter.instruction("call closesocket"); // release the raw parent socket exactly once + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je .Lsys_close_socket_fail"); // translate Winsock error + emitter.instruction("cdqe"); // return zero on successful close + emitter.instruction("add rsp, 56"); // restore stack + emitter.instruction("ret"); // return success + emitter.label(".Lsys_close_socket_fail"); + emitter.instruction("call __rt_wsa_capture_errno"); // publish WSAGetLastError as POSIX errno + emitter.instruction("add rsp, 56"); // restore stack + emitter.instruction("ret"); // return -1 from errno helper + emitter.blank(); +} + +/// Stores the `st_blksize`/`st_blocks` values PHP reports on Windows. +/// +/// Windows has neither `st_blksize` nor `st_blocks` in its stat structure, so +/// php-src compiles the `#else` arms of ext/standard/filestat.c php_stat and +/// reports -1 for both (`HAVE_STRUCT_STAT_ST_BLKSIZE` / +/// `HAVE_STRUCT_STAT_ST_BLOCKS` are undefined there). Leaving the zeroed buffer +/// alone reported 0, which reads as a real answer: `if ($s['blksize'] > 0)` takes +/// the wrong branch and a chunked-copy loop sized from it divides by zero. +/// +/// Expects the caller's stat destination in `rsi`, which is where all three +/// Windows stat shims keep it after clearing the buffer. +fn emit_stat_unsupported_block_fields(emitter: &mut Emitter) { + let blksize_off = emitter.platform.stat_blksize_offset(); + let blocks_off = emitter.platform.stat_blocks_offset(); + emitter.instruction(&format!("mov QWORD PTR [rsi + {}], -1", blksize_off)); // php reports -1 where the platform has no st_blksize + emitter.instruction(&format!("mov QWORD PTR [rsi + {}], -1", blocks_off)); // php reports -1 where the platform has no st_blocks +} + +/// Emits a shim that fills a Linux-layout `struct stat` buffer for an open fd. +/// +/// SysV: rdi=fd (a CRT descriptor in this runtime), rsi=stat buffer. +/// Converts the descriptor to a HANDLE and reads `BY_HANDLE_FILE_INFORMATION`, +/// which supplies the size, attributes, volume, file index, link count, and +/// timestamps for ordinary files and directories. Pipe and character handles +/// do not universally support that query, so `GetFileType` provides a +/// type-correct zero-metadata fallback. Uses Win32 directly instead of msvcrt +/// `fstat`: msvcrt expects a CRT layout that differs from the runtime Linux +/// layout, and the `fstat` C-symbol name is a local shim stub. +pub(super) fn emit_shim_fstat(emitter: &mut Emitter) { + let mode_off = emitter.platform.stat_mode_offset(); + let size_off = emitter.platform.stat_size_offset(); + let nlink_off = emitter.platform.stat_nlink_offset(); + let ino_off = emitter.platform.stat_ino_offset(); + let dev_off = emitter.platform.stat_dev_offset(); + let atime_off = emitter.platform.stat_atime_offset(); + let mtime_off = emitter.platform.stat_mtime_offset(); + let ctime_off = emitter.platform.stat_ctime_offset(); + emitter.label_global("__rt_sys_fstat"); + emitter.instruction("sub rsp, 120"); // shadow(32), BY_HANDLE_FILE_INFORMATION(52), and aligned local slots + emitter.instruction("mov QWORD PTR [rsp + 88], rsi"); // preserve the SysV stat destination across helper calls + emitter.instruction("call __rt_fd_to_handle"); // convert the CRT descriptor to its Win32 HANDLE + emitter.instruction("cmp rax, -1"); // invalid CRT descriptor? + emitter.instruction("je .Lfstat_bad_fd"); // publish EBADF without querying Win32 + emitter.instruction("mov QWORD PTR [rsp + 96], rax"); // retain the handle across metadata calls + emitter.instruction("cld"); // forward direction for rep stosb + emitter.instruction("lea rdi, [rsp + 32]"); // destination = BY_HANDLE_FILE_INFORMATION buffer + emitter.instruction("xor eax, eax"); // zero fill byte + emitter.instruction("mov ecx, 52"); // sizeof(BY_HANDLE_FILE_INFORMATION) + emitter.instruction("rep stosb"); // clear the metadata buffer before Win32 fills it + emitter.instruction("mov rcx, QWORD PTR [rsp + 96]"); // hFile = converted descriptor handle + emitter.instruction("lea rdx, [rsp + 32]"); // lpFileInformation = metadata buffer + emitter.instruction("call GetFileInformationByHandle"); // query real file metadata from the open handle + emitter.instruction("test eax, eax"); // did the metadata query succeed? + emitter.instruction("jnz .Lfstat_metadata"); // populate every available stat field + emitter.instruction("mov rcx, QWORD PTR [rsp + 96]"); // hFile for the pipe/console fallback query + emitter.instruction("call GetFileType"); // identify handles without file-information support + emitter.instruction("test eax, eax"); // FILE_TYPE_UNKNOWN (0)? + emitter.instruction("jz .Lfstat_type_fail"); // preserve and translate the native failure + emitter.instruction("mov DWORD PTR [rsp + 104], eax"); // retain FILE_TYPE_* while the stat buffer is cleared + // -- zero the Linux-layout stat buffer before filling fields -- + emitter.instruction("cld"); // forward direction for rep stosb + emitter.instruction("mov rsi, QWORD PTR [rsp + 88]"); // restore the caller's stat destination + emitter.instruction("mov rdi, rsi"); // dest = stat buffer base + emitter.instruction("xor eax, eax"); // zero fill byte + emitter.instruction("mov ecx, 128"); // Linux struct stat size in bytes + emitter.instruction("rep stosb"); // zero the whole stat buffer + emit_stat_unsupported_block_fields(emitter); + // -- type-correct fallback for pipe and character handles -- + emitter.instruction("mov eax, DWORD PTR [rsp + 104]"); // FILE_TYPE_* result from GetFileType + emitter.instruction("cmp eax, 3"); // FILE_TYPE_PIPE? + emitter.instruction("je .Lfstat_pipe_mode"); // synthesize S_IFIFO + emitter.instruction("cmp eax, 2"); // FILE_TYPE_CHAR? + emitter.instruction("je .Lfstat_char_mode"); // synthesize S_IFCHR + emitter.instruction("mov eax, 0x81B6"); // FILE_TYPE_DISK fallback = S_IFREG | 0666 + emitter.instruction("jmp .Lfstat_fallback_mode_done"); // store the disk fallback mode + emitter.label(".Lfstat_pipe_mode"); + emitter.instruction("mov eax, 0x11B6"); // st_mode = S_IFIFO | 0666 + emitter.instruction("jmp .Lfstat_fallback_mode_done"); // avoid the character fallback + emitter.label(".Lfstat_char_mode"); + emitter.instruction("mov eax, 0x21B6"); // st_mode = S_IFCHR | 0666 + emitter.label(".Lfstat_fallback_mode_done"); + emitter.instruction(&format!("mov DWORD PTR [rsi + {}], eax", mode_off)); // store the fallback object type and permissions + emitter.instruction(&format!("mov DWORD PTR [rsi + {}], 1", nlink_off)); // synthetic handles have one observable link + emitter.instruction(&format!("mov QWORD PTR [rsi + {}], 1", dev_off)); // stable nonzero synthetic device id + emitter.instruction("xor eax, eax"); // return success with the best available metadata + emitter.instruction("add rsp, 120"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lfstat_metadata"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 96]"); // hFile for type classification alongside real metadata + emitter.instruction("call GetFileType"); // distinguish disk files from pipes and character devices + emitter.instruction("mov DWORD PTR [rsp + 104], eax"); // retain FILE_TYPE_* across the stat-buffer zero fill + // -- zero the Linux-layout stat buffer before filling real metadata -- + emitter.instruction("cld"); // forward direction for rep stosb + emitter.instruction("mov rsi, QWORD PTR [rsp + 88]"); // restore the caller's stat destination + emitter.instruction("mov rdi, rsi"); // dest = stat buffer base + emitter.instruction("xor eax, eax"); // zero fill byte + emitter.instruction("mov ecx, 128"); // Linux struct stat size in bytes + emitter.instruction("rep stosb"); // zero the whole stat buffer + emit_stat_unsupported_block_fields(emitter); + // -- st_size, st_dev, st_ino, and st_nlink from BY_HANDLE_FILE_INFORMATION -- + emitter.instruction("mov eax, DWORD PTR [rsp + 68]"); // nFileSizeLow + emitter.instruction("mov edx, DWORD PTR [rsp + 64]"); // nFileSizeHigh + emitter.instruction("shl rdx, 32"); // position the size high dword + emitter.instruction("or rax, rdx"); // combine into the full 64-bit size + emitter.instruction(&format!("mov QWORD PTR [rsi + {}], rax", size_off)); // store st_size + emitter.instruction("mov eax, DWORD PTR [rsp + 60]"); // dwVolumeSerialNumber + emitter.instruction("test eax, eax"); // Wine can expose zero for synthetic volumes + emitter.instruction("jnz .Lfstat_dev_ready"); // preserve a real volume serial + emitter.instruction("mov eax, 1"); // stable nonzero device fallback + emitter.label(".Lfstat_dev_ready"); + emitter.instruction(&format!("mov QWORD PTR [rsi + {}], rax", dev_off)); // store st_dev + emitter.instruction("mov eax, DWORD PTR [rsp + 76]"); // nFileIndexHigh + emitter.instruction("shl rax, 32"); // position the inode high dword + emitter.instruction("mov ecx, DWORD PTR [rsp + 80]"); // nFileIndexLow + emitter.instruction("or rax, rcx"); // combine into a 64-bit inode number + emitter.instruction(&format!("mov QWORD PTR [rsi + {}], rax", ino_off)); // store st_ino + emitter.instruction("mov eax, DWORD PTR [rsp + 72]"); // nNumberOfLinks + emitter.instruction("test eax, eax"); // zero is not a useful POSIX link count + emitter.instruction("jnz .Lfstat_nlink_ready"); // retain a native nonzero count + emitter.instruction("mov eax, 1"); // synthesize a minimum valid link count + emitter.label(".Lfstat_nlink_ready"); + emitter.instruction(&format!("mov DWORD PTR [rsi + {}], eax", nlink_off)); // store st_nlink + // -- st_mode: file/directory/pipe/character type and readonly permission bits -- + emitter.instruction("mov edx, DWORD PTR [rsp + 32]"); // dwFileAttributes + emitter.instruction("mov ecx, DWORD PTR [rsp + 104]"); // FILE_TYPE_* from the open handle + emitter.instruction("cmp ecx, 3"); // FILE_TYPE_PIPE? + emitter.instruction("je .Lfstat_metadata_pipe_mode"); // pipes are FIFO objects, not regular files + emitter.instruction("cmp ecx, 2"); // FILE_TYPE_CHAR? + emitter.instruction("je .Lfstat_metadata_char_mode"); // consoles and devices are character objects + emitter.instruction("mov eax, 0x81B6"); // default st_mode = S_IFREG | 0666 + emitter.instruction("test edx, 0x10"); // FILE_ATTRIBUTE_DIRECTORY set? + emitter.instruction("jz .Lfstat_mode_readonly"); // non-directory keeps regular-file type + emitter.instruction("mov eax, 0x41FF"); // st_mode = S_IFDIR | 0777 + emitter.instruction("jmp .Lfstat_mode_readonly"); // apply readonly bits uniformly after type selection + emitter.label(".Lfstat_metadata_pipe_mode"); + emitter.instruction("mov eax, 0x11B6"); // st_mode = S_IFIFO | 0666 + emitter.instruction("jmp .Lfstat_mode_readonly"); // apply readonly bits uniformly after type selection + emitter.label(".Lfstat_metadata_char_mode"); + emitter.instruction("mov eax, 0x21B6"); // st_mode = S_IFCHR | 0666 + emitter.label(".Lfstat_mode_readonly"); + emitter.instruction("test edx, 1"); // FILE_ATTRIBUTE_READONLY set? + emitter.instruction("jz .Lfstat_mode_done"); // writable objects retain synthesized write bits + emitter.instruction("and eax, 0xFF6D"); // readonly objects clear 0222 while retaining type/read bits + emitter.label(".Lfstat_mode_done"); + emitter.instruction(&format!("mov DWORD PTR [rsi + {}], eax", mode_off)); // store st_mode + // -- timestamps: convert FILETIME values to Unix epoch seconds -- + emit_filetime_to_stat_seconds(emitter, 44, atime_off); + emit_filetime_to_stat_seconds(emitter, 52, mtime_off); + emit_filetime_to_stat_seconds(emitter, 36, ctime_off); + emitter.instruction("xor eax, eax"); // return 0 (success) + emitter.instruction("add rsp, 120"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lfstat_type_fail"); + emitter.instruction("call GetLastError"); // capture the fallback query failure + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // retain the native diagnostic + emitter.instruction("call __rt_win32_errno_from_code"); // translate the native error to POSIX errno + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish observable errno + emitter.instruction("mov rax, -1"); // return -1 on failure + emitter.instruction("add rsp, 120"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lfstat_bad_fd"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 9"); // EBADF: descriptor has no Win32 HANDLE + emitter.instruction("mov rax, -1"); // return -1 for an invalid descriptor + emitter.instruction("add rsp, 120"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits a shim that converts `open(path, flags, mode)` to `CreateFileW`. +/// +/// SysV: rdi=path, rsi=flags, rdx=mode. +/// Maps Linux open flags to Win32 `CreateFileW` parameters after strict UTF-8 conversion: +/// - O_RDONLY(0) → GENERIC_READ, OPEN_EXISTING +/// - O_WRONLY(1) → GENERIC_WRITE, OPEN_EXISTING +/// - O_RDWR(2) → GENERIC_READ|GENERIC_WRITE, OPEN_EXISTING +/// - O_CREAT(0x40) → OPEN_ALWAYS (or CREATE_ALWAYS with O_TRUNC) +/// - O_CREAT|O_EXCL(0xC0) → CREATE_NEW +/// - O_TRUNC(0x200) → TRUNCATE_EXISTING (or CREATE_ALWAYS with O_CREAT) +/// - O_APPEND(0x400) → FILE_APPEND_DATA +pub(super) fn emit_shim_open(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_open"); + emitter.instruction("sub rsp, 88"); // shadow, CreateFile stack args, and aligned owned-path locals + emitter.instruction("mov QWORD PTR [rsp + 64], rsi"); // preserve flags across strict conversion + emitter.instruction("call __rt_win_utf8_to_utf16"); // convert the PHP UTF-8 path to an owned wide path + emitter.instruction("test rax, rax"); // conversion succeeded? + emitter.instruction("jz .Lopen_conversion_fail"); // invalid UTF-8 path + emitter.instruction("mov QWORD PTR [rsp + 56], rax"); // retain the owned wide path through CreateFileW + emitter.instruction("mov rsi, QWORD PTR [rsp + 64]"); // restore Linux open flags + // -- determine dwDesiredAccess in rax -- + emitter.instruction("mov rax, 0x80000000"); // GENERIC_READ (default) + emitter.instruction("test rsi, 1"); // O_WRONLY? + emitter.instruction("jnz .Lopen_wr"); // → GENERIC_WRITE + emitter.instruction("test rsi, 2"); // O_RDWR? + emitter.instruction("jnz .Lopen_rw"); // → GENERIC_READ|GENERIC_WRITE + emitter.instruction("jmp .Lopen_access_done"); // → use GENERIC_READ + emitter.label(".Lopen_wr"); + emitter.instruction("mov rax, 0x40000000"); // GENERIC_WRITE + emitter.instruction("jmp .Lopen_access_done"); // → proceed + emitter.label(".Lopen_rw"); + emitter.instruction("mov rax, 0xC0000000"); // GENERIC_READ|GENERIC_WRITE + emitter.label(".Lopen_access_done"); + emitter.instruction("test rsi, 0x400"); // O_APPEND? + emitter.instruction("jz .Lopen_no_append"); // skip if not append + emitter.instruction("or rax, 0x4"); // FILE_APPEND_DATA + emitter.label(".Lopen_no_append"); + // -- determine dwCreationDisposition in r10 -- + emitter.instruction("test rsi, 0x40"); // O_CREAT? + emitter.instruction("jz .Lopen_no_creat"); // → no create + emitter.instruction("test rsi, 0x80"); // O_EXCL with O_CREAT? + emitter.instruction("jnz .Lopen_create_new"); // → CREATE_NEW, never replace an existing file + emitter.instruction("test rsi, 0x200"); // O_CREAT + O_TRUNC? + emitter.instruction("jnz .Lopen_create_always"); // → CREATE_ALWAYS + emitter.instruction("mov r10, 4"); // OPEN_ALWAYS (create or open) + emitter.instruction("jmp .Lopen_disp_done"); // → proceed + emitter.label(".Lopen_create_always"); + emitter.instruction("mov r10, 2"); // CREATE_ALWAYS + emitter.instruction("jmp .Lopen_disp_done"); // → proceed + emitter.label(".Lopen_create_new"); + emitter.instruction("mov r10, 1"); // CREATE_NEW: PHP fopen('x') must fail when the path exists + emitter.instruction("jmp .Lopen_disp_done"); // → proceed + emitter.label(".Lopen_no_creat"); + emitter.instruction("test rsi, 0x200"); // O_TRUNC without O_CREAT? + emitter.instruction("jnz .Lopen_trunc_existing"); // → TRUNCATE_EXISTING + emitter.instruction("mov r10, 3"); // OPEN_EXISTING + emitter.instruction("jmp .Lopen_disp_done"); // → proceed + emitter.label(".Lopen_trunc_existing"); + emitter.instruction("mov r10, 5"); // TRUNCATE_EXISTING + emitter.label(".Lopen_disp_done"); + // -- call CreateFileW(rcx=path, rdx=access, r8=share, r9=NULL, [rsp+32]=disp, [rsp+40]=0, [rsp+48]=0) -- + emitter.instruction("mov rcx, QWORD PTR [rsp + 56]"); // lpFileName = owned UTF-16 path + emitter.instruction("mov rdx, rax"); // dwDesiredAccess + emitter.instruction("mov r8, 7"); // PHP_WIN32_IOUTIL_DEFAULT_SHARE_MODE: FILE_SHARE_READ | + // FILE_SHARE_WRITE | FILE_SHARE_DELETE (win32/ioutil.h). Without + // the DELETE bit, unlink() and rename() fail while any handle to + // the file is still open, where php on windows succeeds + emitter.instruction("xor r9, r9"); // lpSecurityAttributes = NULL + emitter.instruction("mov QWORD PTR [rsp + 32], r10"); // dwCreationDisposition + emitter.instruction("mov QWORD PTR [rsp + 40], 0"); // dwFlagsAndAttributes = 0 + emitter.instruction("mov QWORD PTR [rsp + 48], 0"); // hTemplateFile = NULL + emitter.instruction("call CreateFileW"); // open the Unicode path + emitter.instruction("cmp rax, -1"); // INVALID_HANDLE_VALUE? + emitter.instruction("je .Lopen_fail"); // capture native failure before cleanup + emitter.instruction("mov QWORD PTR [rsp + 72], rax"); // preserve the file handle across cleanup + emitter.instruction("mov rax, QWORD PTR [rsp + 56]"); // owned wide path + emitter.instruction("call __rt_heap_free"); // release the converted path + emitter.instruction("mov rcx, QWORD PTR [rsp + 72]"); // Win32 handle transferred into the CRT table + emitter.instruction("mov edx, 0x8000"); // default PHP fopen mode is CRT _O_BINARY + emitter.instruction("test DWORD PTR [rsp + 64], 0x4000"); // did PHP's `t` modifier request CRT text translation? + emitter.instruction("jz .Lopen_crt_text_mode_done"); // retain binary mode unless text was explicit + emitter.instruction("mov edx, 0x4000"); // _O_TEXT enables the Windows CRT text-mode behavior + emitter.label(".Lopen_crt_text_mode_done"); + emitter.instruction("mov eax, DWORD PTR [rsp + 64]"); // Linux access mode flags + emitter.instruction("and eax, 3"); // O_RDONLY/O_WRONLY/O_RDWR match the CRT values + emitter.instruction("or edx, eax"); // preserve the caller's access mode + emitter.instruction("test DWORD PTR [rsp + 64], 0x400"); // Linux O_APPEND? + emitter.instruction("jz .Lopen_crt_flags_done"); // no CRT append flag needed + emitter.instruction("or edx, 8"); // _O_APPEND + emitter.label(".Lopen_crt_flags_done"); + emitter.instruction("call _open_osfhandle"); // adopt HANDLE as a CRT file descriptor + emitter.instruction("cmp eax, -1"); // CRT descriptor allocation failed? + emitter.instruction("je .Lopen_crt_fail"); // close the still-owned raw handle + emitter.instruction("mov QWORD PTR [rsp + 80], rax"); // preserve the new descriptor while caching status flags + emitter.instruction("mov rdi, rax"); // registry key = CRT descriptor + emitter.instruction("mov rsi, QWORD PTR [rsp + 64]"); // registry value = original Linux open flags + emitter.instruction("call __rt_win_fd_status_upsert"); // retain access mode for F_GETFL metadata + emitter.instruction("mov rax, QWORD PTR [rsp + 80]"); // restore the descriptor result + emitter.instruction("add rsp, 88"); // restore stack + emitter.instruction("ret"); // return CRT file descriptor + emitter.label(".Lopen_crt_fail"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 72]"); // raw HANDLE was not adopted on failure + emitter.instruction("call CloseHandle"); // avoid leaking the failed descriptor's handle + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 24"); // EMFILE: CRT descriptor table allocation failed + emitter.instruction("mov rax, -1"); // POSIX open failure + emitter.instruction("add rsp, 88"); // restore stack + emitter.instruction("ret"); // return failure + emitter.label(".Lopen_fail"); + emitter.instruction("call GetLastError"); // fetch native failure before cleanup can clobber it + emitter.instruction("mov DWORD PTR [rsp + 80], eax"); // preserve the native error code + emitter.instruction("mov rax, QWORD PTR [rsp + 56]"); // owned wide path + emitter.instruction("call __rt_heap_free"); // release the converted path on failure + emitter.instruction("mov eax, DWORD PTR [rsp + 80]"); // restore native error code + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // retain native Win32 state + emitter.instruction("call __rt_win32_errno_from_code"); // translate to POSIX errno + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish observable errno + emitter.instruction("mov rax, -1"); // POSIX open failure + emitter.instruction("add rsp, 88"); // restore stack + emitter.instruction("ret"); // return failure + emitter.label(".Lopen_conversion_fail"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 84"); // EILSEQ: invalid UTF-8 path + emitter.instruction("mov rax, -1"); // POSIX open failure + emitter.instruction("add rsp, 88"); // restore stack + emitter.instruction("ret"); // return failure + emitter.blank(); +} + +/// Emits a shim that converts `lseek` to the 64-bit `SetFilePointerEx` API. +/// +/// SysV: rdi=fd, rsi=offset, rdx=whence. +/// Maps SEEK_SET(0)→FILE_BEGIN(0), SEEK_CUR(1)→FILE_CURRENT(1), SEEK_END(2)→FILE_END(2). +/// The signed 64-bit distance and resulting absolute position are preserved +/// without the 2 GiB truncation of the legacy `SetFilePointer` shim. Native +/// failures are translated into the runtime errno channel and return `-1`. +pub(super) fn emit_shim_lseek(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_lseek"); + emitter.instruction("sub rsp, 72"); // shadow space plus aligned offset/whence/result/error locals + emitter.instruction("mov QWORD PTR [rsp + 32], rsi"); // preserve signed 64-bit distance across handle conversion + emitter.instruction("mov QWORD PTR [rsp + 40], rdx"); // preserve whence across handle conversion + emitter.instruction("mov rcx, rdi"); // fd + emitter.instruction("call __rt_fd_to_handle"); // convert to HANDLE + emitter.instruction("mov rcx, rax"); // handle + emitter.instruction("mov rdx, QWORD PTR [rsp + 32]"); // signed LARGE_INTEGER distance + emitter.instruction("lea r8, [rsp + 48]"); // receive the full-width new file position + emitter.instruction("mov r9d, DWORD PTR [rsp + 40]"); // FILE_BEGIN/FILE_CURRENT/FILE_END + emitter.instruction("call SetFilePointerEx"); // seek without truncating offsets above 2 GiB + emitter.instruction("test eax, eax"); // Win32 BOOL success? + emitter.instruction("jz .Llseek_fail"); // translate native failure + emitter.instruction("mov rax, QWORD PTR [rsp + 48]"); // return the 64-bit absolute position + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return new position + emitter.label(".Llseek_fail"); + emitter.instruction("call GetLastError"); // capture the native seek failure + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // retain native Win32 state + emitter.instruction("call __rt_win32_errno_from_code"); // translate failure to POSIX errno + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish observable errno + emitter.instruction("mov rax, -1"); // POSIX lseek failure + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return failure + emitter.blank(); +} + +/// Emits the bounded descriptor-status registry used by the Win32 fcntl shim. +pub(super) fn emit_win_fd_status_registry(emitter: &mut Emitter) { + emitter.label_global("__rt_win_fd_status_find"); + emitter.instruction("lea rax, [rip + _win_fd_status_records]"); // first cached descriptor-status record + emitter.instruction("mov ecx, 64"); // bounded registry capacity + emitter.label(".Lwin_fd_status_find_loop"); + emitter.instruction("cmp QWORD PTR [rax], 0"); // active record? + emitter.instruction("je .Lwin_fd_status_find_next"); // skip unused records + emitter.instruction("cmp QWORD PTR [rax + 8], rdi"); // same descriptor or opaque SOCKET? + emitter.instruction("je .Lwin_fd_status_find_done"); // return the matching record + emitter.label(".Lwin_fd_status_find_next"); + emitter.instruction("add rax, 24"); // advance to the next record + emitter.instruction("dec ecx"); // one fewer record remains + emitter.instruction("jnz .Lwin_fd_status_find_loop"); // scan the bounded table + emitter.instruction("xor eax, eax"); // no cached status exists + emitter.label(".Lwin_fd_status_find_done"); + emitter.instruction("ret"); // return record or NULL + emitter.blank(); + + emitter.label_global("__rt_win_fd_status_upsert"); + emitter.instruction("lea rax, [rip + _win_fd_status_records]"); // first cached descriptor-status record + emitter.instruction("xor r8d, r8d"); // no free record selected yet + emitter.instruction("mov ecx, 64"); // bounded registry capacity + emitter.label(".Lwin_fd_status_upsert_loop"); + emitter.instruction("cmp QWORD PTR [rax], 0"); // unused record? + emitter.instruction("jne .Lwin_fd_status_upsert_match"); // occupied records may match the key + emitter.instruction("test r8, r8"); // already retained an earlier free record? + emitter.instruction("cmovz r8, rax"); // remember the first available slot + emitter.instruction("jmp .Lwin_fd_status_upsert_next"); // continue looking for an existing key + emitter.label(".Lwin_fd_status_upsert_match"); + emitter.instruction("cmp QWORD PTR [rax + 8], rdi"); // same descriptor or opaque SOCKET? + emitter.instruction("je .Lwin_fd_status_upsert_store"); // overwrite its status flags + emitter.label(".Lwin_fd_status_upsert_next"); + emitter.instruction("add rax, 24"); // advance to the next record + emitter.instruction("dec ecx"); // one fewer record remains + emitter.instruction("jnz .Lwin_fd_status_upsert_loop"); // scan the bounded table + emitter.instruction("mov rax, r8"); // use the first free record, if any + emitter.instruction("test rax, rax"); // registry exhausted? + emitter.instruction("jz .Lwin_fd_status_upsert_done"); // silently leave status uncached + emitter.label(".Lwin_fd_status_upsert_store"); + emitter.instruction("mov QWORD PTR [rax], 1"); // mark the record active + emitter.instruction("mov QWORD PTR [rax + 8], rdi"); // cache descriptor/SOCKET key + emitter.instruction("mov QWORD PTR [rax + 16], rsi"); // cache Linux status flags + emitter.label(".Lwin_fd_status_upsert_done"); + emitter.instruction("ret"); // return record or NULL on exhaustion + emitter.blank(); + + emitter.label_global("__rt_win_fd_status_clear"); + emitter.instruction("sub rsp, 8"); // align the internal lookup call + emitter.instruction("call __rt_win_fd_status_find"); // find cached state for the closing descriptor + emitter.instruction("add rsp, 8"); // restore the caller stack + emitter.instruction("test rax, rax"); // matching record found? + emitter.instruction("jz .Lwin_fd_status_clear_done"); // nothing was cached + emitter.instruction("mov QWORD PTR [rax], 0"); // release the registry record + emitter.label(".Lwin_fd_status_clear_done"); + emitter.instruction("ret"); // return after cache invalidation + emitter.blank(); +} + +/// Emits a Win32 implementation of Linux `fcntl(F_GETFL/F_SETFL)`. +/// +/// File access flags are retained from `open`; socket nonblocking changes are +/// applied with `ioctlsocket(FIONBIO)` and cached for subsequent F_GETFL calls. +pub(super) fn emit_shim_fcntl(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_fcntl"); + emitter.instruction("sub rsp, 72"); // shadow space plus descriptor, command, flags, and ioctl value + emitter.instruction("mov QWORD PTR [rsp + 32], rdi"); // preserve descriptor/SOCKET + emitter.instruction("mov QWORD PTR [rsp + 40], rsi"); // preserve fcntl command + emitter.instruction("mov QWORD PTR [rsp + 48], rdx"); // preserve proposed status flags + emitter.instruction("cmp esi, 3"); // F_GETFL? + emitter.instruction("je .Lfcntl_getfl"); // return cached status flags + emitter.instruction("cmp esi, 4"); // F_SETFL? + emitter.instruction("je .Lfcntl_setfl"); // update file/socket status flags + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 22"); // EINVAL for unsupported fcntl commands + emitter.instruction("mov rax, -1"); // unsupported operation + emitter.instruction("jmp .Lfcntl_done"); // return failure + emitter.label(".Lfcntl_getfl"); + emitter.instruction("call __rt_win_fd_status_find"); // query cached flags for this descriptor + emitter.instruction("test rax, rax"); // cached record available? + emitter.instruction("jnz .Lfcntl_getfl_cached"); // return its exact Linux flags + emitter.instruction("mov rdi, QWORD PTR [rsp + 32]"); // restore descriptor for HANDLE detection + emitter.instruction("mov rcx, rdi"); // CRT descriptor argument + emitter.instruction("call _get_osfhandle"); // distinguish CRT descriptors from Winsock SOCKETs + emitter.instruction("cmp rax, -1"); // socket/invalid descriptor has no CRT HANDLE + emitter.instruction("je .Lfcntl_getfl_socket"); // sockets are read-write and initially blocking + emitter.instruction("xor esi, esi"); // uncached CRT descriptor defaults to O_RDONLY + emitter.instruction("jmp .Lfcntl_getfl_seed"); // cache the inferred flags + emitter.label(".Lfcntl_getfl_socket"); + emitter.instruction("mov esi, 2"); // Winsock sockets have O_RDWR semantics + emitter.label(".Lfcntl_getfl_seed"); + emitter.instruction("mov rdi, QWORD PTR [rsp + 32]"); // registry key + emitter.instruction("call __rt_win_fd_status_upsert"); // cache inferred initial state + emitter.instruction("mov rax, rsi"); // return inferred Linux status flags + emitter.instruction("jmp .Lfcntl_done"); // finish F_GETFL + emitter.label(".Lfcntl_getfl_cached"); + emitter.instruction("mov rax, QWORD PTR [rax + 16]"); // return cached Linux status flags + emitter.instruction("jmp .Lfcntl_done"); // finish F_GETFL + emitter.label(".Lfcntl_setfl"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 32]"); // descriptor for CRT HANDLE detection + emitter.instruction("call _get_osfhandle"); // files need cache-only changes; sockets need FIONBIO + emitter.instruction("cmp rax, -1"); // no CRT HANDLE means a Winsock SOCKET + emitter.instruction("jne .Lfcntl_setfl_cache"); // regular files do not need a native nonblocking operation + emitter.instruction("mov rax, QWORD PTR [rsp + 48]"); // proposed Linux status flags + emitter.instruction("shr rax, 11"); // move O_NONBLOCK (0x800) to bit zero + emitter.instruction("and eax, 1"); // ULONG mode = 0/1 + emitter.instruction("mov DWORD PTR [rsp + 56], eax"); // stable FIONBIO value storage + emitter.instruction("mov rcx, QWORD PTR [rsp + 32]"); // Winsock SOCKET + emitter.instruction("mov edx, 0x8004667e"); // FIONBIO + emitter.instruction("lea r8, [rsp + 56]"); // pointer to ULONG nonblocking mode + emitter.instruction("call ioctlsocket"); // update native socket blocking state + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je .Lfcntl_setfl_fail"); // publish Winsock failure + emitter.label(".Lfcntl_setfl_cache"); + emitter.instruction("mov rdi, QWORD PTR [rsp + 32]"); // registry key + emitter.instruction("mov rsi, QWORD PTR [rsp + 48]"); // updated Linux status flags + emitter.instruction("call __rt_win_fd_status_upsert"); // preserve F_GETFL-visible state + emitter.instruction("xor eax, eax"); // F_SETFL success + emitter.instruction("jmp .Lfcntl_done"); // return success + emitter.label(".Lfcntl_setfl_fail"); + emitter.instruction("call __rt_wsa_capture_errno"); // translate WSAGetLastError and return -1 + emitter.label(".Lfcntl_done"); + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return fcntl result + emitter.blank(); +} + +/// Emits a shim that converts `unlink` to `DeleteFileW`. +pub(super) fn emit_shim_unlink(emitter: &mut Emitter) { + emit_wide_path_bool_shim(emitter, "__rt_sys_unlink", "DeleteFileW", false); +} + +/// Emits a POSIX `getcwd` shim backed by a dynamically sized `GetCurrentDirectoryW` buffer. +pub(super) fn emit_shim_getcwd(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_getcwd"); + emitter.instruction("sub rsp, 88"); // shadow space and aligned destination/wide/error locals + emitter.instruction("mov QWORD PTR [rsp + 32], rdi"); // preserve caller UTF-8 destination + emitter.instruction("mov QWORD PTR [rsp + 40], rsi"); // preserve destination byte capacity + emitter.instruction("xor ecx, ecx"); // query the required WCHAR capacity + emitter.instruction("xor edx, edx"); // no destination for the size query + emitter.instruction("call GetCurrentDirectoryW"); // required WCHAR count including NUL + emitter.instruction("test eax, eax"); // size query succeeded? + emitter.instruction("jz .Lgetcwd_query_fail"); // capture native query failure + emitter.instruction("mov DWORD PTR [rsp + 56], eax"); // retain WCHAR capacity + emitter.instruction("movsxd rax, eax"); // widen WCHAR count for allocation + emitter.instruction("shl rax, 1"); // WCHAR uses two bytes + emitter.instruction("call __rt_heap_alloc"); // allocate dynamic wide buffer + emitter.instruction("test rax, rax"); // allocation succeeded? + emitter.instruction("jz .Lgetcwd_alloc_fail"); // ENOMEM + emitter.instruction("mov QWORD PTR [rsp + 48], rax"); // retain owned wide buffer + emitter.instruction("mov ecx, DWORD PTR [rsp + 56]"); // WCHAR capacity + emitter.instruction("mov rdx, rax"); // wide destination + emitter.instruction("call GetCurrentDirectoryW"); // retrieve the current directory as UTF-16 + emitter.instruction("test eax, eax"); // native retrieval succeeded? + emitter.instruction("jz .Lgetcwd_wide_fail"); // capture native failure + emitter.instruction("cmp eax, DWORD PTR [rsp + 56]"); // did the directory grow after the size query? + emitter.instruction("jae .Lgetcwd_range_fail"); // caller-visible range failure + emitter.instruction("mov rdi, QWORD PTR [rsp + 48]"); // UTF-16 source + emitter.instruction("mov rsi, QWORD PTR [rsp + 32]"); // caller UTF-8 destination + emitter.instruction("mov rdx, QWORD PTR [rsp + 40]"); // caller destination byte capacity + emitter.instruction("call __rt_win_utf16_to_utf8"); // strict UTF-16 to UTF-8 conversion including NUL + emitter.instruction("test eax, eax"); // conversion fit and succeeded? + emitter.instruction("jz .Lgetcwd_wide_fail"); // capture conversion/range failure + emitter.instruction("mov rax, QWORD PTR [rsp + 48]"); // owned wide buffer + emitter.instruction("call __rt_heap_free"); // release temporary UTF-16 storage + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // POSIX getcwd returns the destination pointer + emitter.instruction("add rsp, 88"); // restore stack + emitter.instruction("ret"); // return UTF-8 destination + emitter.label(".Lgetcwd_wide_fail"); + emitter.instruction("call GetLastError"); // capture failure before cleanup + emitter.instruction("mov DWORD PTR [rsp + 64], eax"); // retain native error code + emitter.instruction("mov rax, QWORD PTR [rsp + 48]"); // owned wide buffer + emitter.instruction("call __rt_heap_free"); // release temporary UTF-16 storage + emitter.instruction("mov eax, DWORD PTR [rsp + 64]"); // restore native error + emitter.instruction("jmp .Lgetcwd_publish_native_error"); // map error and return NULL + emitter.label(".Lgetcwd_query_fail"); + emitter.instruction("call GetLastError"); // capture size-query failure + emitter.label(".Lgetcwd_publish_native_error"); + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // retain native Win32 state + emitter.instruction("call __rt_win32_errno_from_code"); // translate to POSIX errno + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish observable errno + emitter.instruction("xor eax, eax"); // POSIX getcwd failure is NULL + emitter.instruction("add rsp, 88"); // restore stack + emitter.instruction("ret"); // return failure + emitter.label(".Lgetcwd_range_fail"); + emitter.instruction("mov rax, QWORD PTR [rsp + 48]"); // owned wide buffer + emitter.instruction("call __rt_heap_free"); // release buffer after a size race + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 34"); // ERANGE + emitter.instruction("xor eax, eax"); // POSIX getcwd failure is NULL + emitter.instruction("add rsp, 88"); // restore stack + emitter.instruction("ret"); // return failure + emitter.label(".Lgetcwd_alloc_fail"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 12"); // ENOMEM + emitter.instruction("xor eax, eax"); // POSIX getcwd failure is NULL + emitter.instruction("add rsp, 88"); // restore stack + emitter.instruction("ret"); // return failure + emitter.blank(); +} + +/// Emits a shim that wraps `SetCurrentDirectoryW`. +pub(super) fn emit_shim_chdir(emitter: &mut Emitter) { + emit_wide_path_bool_shim( + emitter, + "__rt_sys_chdir", + "SetCurrentDirectoryW", + false, + ); +} + +/// Emits a shim that converts `mkdir` to `CreateDirectoryW`. +pub(super) fn emit_shim_mkdir(emitter: &mut Emitter) { + emit_wide_path_bool_shim(emitter, "__rt_sys_mkdir", "CreateDirectoryW", true); +} + +/// Emits a shim that converts `rmdir` to `RemoveDirectoryW`. +pub(super) fn emit_shim_rmdir(emitter: &mut Emitter) { + emit_wide_path_bool_shim(emitter, "__rt_sys_rmdir", "RemoveDirectoryW", false); +} + +/// Emits a one-path Win32 filesystem shim with POSIX status translation and errno capture. +fn emit_wide_path_bool_shim( + emitter: &mut Emitter, + label: &str, + api: &str, + null_second_arg: bool, +) { + let fail_label = format!(".L{}_wide_fail", label.trim_start_matches("__rt_sys_")); + let conversion_label = format!( + ".L{}_wide_conversion_fail", + label.trim_start_matches("__rt_sys_") + ); + emitter.label_global(label); + emitter.instruction("sub rsp, 56"); // shadow space and aligned owned-path locals + emitter.instruction("call __rt_win_utf8_to_utf16"); // convert SysV rdi UTF-8 path strictly + emitter.instruction("test rax, rax"); // conversion succeeded? + emitter.instruction(&format!("jz {conversion_label}")); // invalid UTF-8 path + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // retain owned wide path + emitter.instruction("mov rcx, rax"); // native path argument + if null_second_arg { + emitter.instruction("xor edx, edx"); // optional security attributes = NULL + } + emitter.instruction(&format!("call {api}")); // invoke native Unicode filesystem API + emitter.instruction("test eax, eax"); // Win32 BOOL success? + emitter.instruction(&format!("jz {fail_label}")); // capture the native failure + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // owned wide path + emitter.instruction("call __rt_heap_free"); // release converted path + emitter.instruction("xor eax, eax"); // translate Win32 TRUE to POSIX success + emitter.instruction("add rsp, 56"); // restore stack + emitter.instruction("ret"); // return zero for POSIX success + emitter.label(&fail_label); + emitter.instruction("call GetLastError"); // fetch error before cleanup can clobber it + emitter.instruction("mov DWORD PTR [rsp + 40], eax"); // preserve native error + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // owned wide path + emitter.instruction("call __rt_heap_free"); // release converted path on failure + emitter.instruction("mov eax, DWORD PTR [rsp + 40]"); // reload native error + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // retain native Win32 state + emitter.instruction("call __rt_win32_errno_from_code"); // translate to POSIX errno + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish observable errno + emitter.instruction("mov eax, -1"); // translate Win32 FALSE to POSIX failure + emitter.instruction("cdqe"); // sign-extend the POSIX int result for 64-bit runtime consumers + emitter.instruction("add rsp, 56"); // restore stack + emitter.instruction("ret"); // return -1 for POSIX failure + emitter.label(&conversion_label); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 84"); // EILSEQ: invalid UTF-8 path + emitter.instruction("mov eax, -1"); // invalid paths use the POSIX failure sentinel + emitter.instruction("cdqe"); // sign-extend the POSIX int result for 64-bit runtime consumers + emitter.instruction("add rsp, 56"); // restore stack + emitter.instruction("ret"); // return -1 for POSIX failure + emitter.blank(); +} + +/// Emits a shim that fills a Linux-layout `struct stat` buffer for a path. +/// +/// SysV: rdi=path, rsi=stat buffer. Returns 0 on success, -1 on failure. +/// Queries the file with Win32 `GetFileAttributesExW` and writes st_mode, +/// st_size, st_nlink, st_ino, and the atime/mtime/ctime seconds at the +/// Windows-target `struct stat` offsets the runtime reads. Uses Win32 directly +/// instead of msvcrt `stat`: the msvcrt struct layout differs from the runtime +/// Linux layout, and the `stat` C-symbol name is a local shim stub, so calling +/// it would recurse. +/// +/// `st_ino` is synthesized from `GetFileInformationByHandle`'s +/// `nFileIndexHigh:nFileIndexLow` (the NTFS/ReFS file-index pair, stable and +/// unique per volume — Windows' closest equivalent to a POSIX inode number), +/// which requires a second Win32 round trip: `CreateFileW` (metadata-only, with +/// `FILE_FLAG_BACKUP_SEMANTICS` so directories open too) to get a handle, then +/// `GetFileInformationByHandle`, then `CloseHandle`. The owned UTF-16 path is +/// retained in a stack slot because the zero-fill loops reuse `rdi` as their +/// `rep stosb` destination; `rsi` (the stat buffer base) is MSx64 non-volatile +/// and survives the native calls untouched. +pub(super) fn emit_shim_stat(emitter: &mut Emitter) { + let mode_off = emitter.platform.stat_mode_offset(); + let size_off = emitter.platform.stat_size_offset(); + let nlink_off = emitter.platform.stat_nlink_offset(); + let ino_off = emitter.platform.stat_ino_offset(); + let dev_off = emitter.platform.stat_dev_offset(); + let atime_off = emitter.platform.stat_atime_offset(); + let mtime_off = emitter.platform.stat_mtime_offset(); + let ctime_off = emitter.platform.stat_ctime_offset(); + emitter.label_global("__rt_sys_stat"); + emitter.instruction("sub rsp, 168"); // shadow(32), path metadata, handle information, and executable/mode locals, 16B aligned + emitter.instruction("mov QWORD PTR [rsp + 144], rsi"); // preserve the SysV stat destination across UTF conversion + emitter.instruction("call __rt_win_utf8_to_utf16"); // strictly convert the PHP UTF-8 path + emitter.instruction("test rax, rax"); // conversion succeeded? + emitter.instruction("jz .Lstat_conversion_fail"); // invalid UTF-8 path + emitter.instruction("mov QWORD PTR [rsp + 72], rax"); // retain the owned wide path for both metadata queries + emitter.instruction("mov rcx, rax"); // lpFileName = UTF-16 path + emitter.instruction("xor edx, edx"); // fInfoLevelId = GetFileExInfoStandard (0) + emitter.instruction("lea r8, [rsp + 32]"); // lpFileInformation = &WIN32_FILE_ATTRIBUTE_DATA + emitter.instruction("call GetFileAttributesExW"); // query Unicode path attributes, size, and timestamps + emitter.instruction("test eax, eax"); // zero return means the query failed + emitter.instruction("jz .Lstat_fail"); // return -1 when the path cannot be queried + // -- zero the Linux-layout stat buffer before filling fields -- + emitter.instruction("cld"); // forward direction for rep stosb + emitter.instruction("mov rsi, QWORD PTR [rsp + 144]"); // restore the caller's stat destination + emitter.instruction("mov rdi, rsi"); // dest = stat buffer base + emitter.instruction("xor eax, eax"); // zero fill byte + emitter.instruction("mov ecx, 128"); // Linux struct stat size in bytes + emitter.instruction("rep stosb"); // zero the whole stat buffer + emit_stat_unsupported_block_fields(emitter); + // -- st_size = (nFileSizeHigh << 32) | nFileSizeLow -- + emitter.instruction("mov eax, DWORD PTR [rsp + 64]"); // nFileSizeLow (zero-extends into rax) + emitter.instruction("mov edx, DWORD PTR [rsp + 60]"); // nFileSizeHigh + emitter.instruction("shl rdx, 32"); // shift the high dword into the upper 32 bits + emitter.instruction("or rax, rdx"); // combine into the full 64-bit size + emitter.instruction(&format!("mov QWORD PTR [rsi + {}], rax", size_off)); // store st_size + // -- st_mode: PHP/Windows synthesizes permissions from attributes and executable suffixes -- + emitter.instruction("mov edx, DWORD PTR [rsp + 32]"); // dwFileAttributes + emitter.instruction("mov eax, 0x81B6"); // default st_mode = S_IFREG | 0666 + emitter.instruction("test edx, 0x10"); // FILE_ATTRIBUTE_DIRECTORY set? + emitter.instruction("jz .Lstat_mode_readonly"); // non-directory keeps regular-file type + emitter.instruction("mov eax, 0x41FF"); // st_mode = S_IFDIR | 0777 + emitter.label(".Lstat_mode_readonly"); + emitter.instruction("test edx, 1"); // FILE_ATTRIBUTE_READONLY set? + emitter.instruction("jz .Lstat_mode_base_ready"); // writable object retains synthesized write bits + emitter.instruction("and eax, 0xFF6D"); // readonly object clears 0222, including directories (0555) + emitter.label(".Lstat_mode_base_ready"); + emitter.instruction("mov DWORD PTR [rsp + 156], eax"); // retain the base mode across GetBinaryTypeW + emitter.instruction("test edx, 0x10"); // directory? + emitter.instruction("jnz .Lstat_mode_done"); // directories are executable by type, not file suffix + emitter.instruction("mov rdi, QWORD PTR [rsp + 72]"); // UTF-16 path for executable-suffix inspection + emitter.instruction("xor rcx, rcx"); // path length in UTF-16 code units + emitter.label(".Lstat_exec_len"); + emitter.instruction("cmp WORD PTR [rdi + rcx * 2], 0"); // trailing NUL reached? + emitter.instruction("je .Lstat_exec_len_done"); // suffix can now be inspected + emitter.instruction("inc rcx"); // count one UTF-16 code unit + emitter.instruction("jmp .Lstat_exec_len"); // continue scanning the owned path + emitter.label(".Lstat_exec_len_done"); + emitter.instruction("cmp rcx, 4"); // extension needs dot plus three characters + emitter.instruction("jb .Lstat_mode_done"); // no executable suffix possible + emitter.instruction("cmp WORD PTR [rdi + rcx * 2 - 8], 46"); // final extension begins with ASCII dot? + emitter.instruction("jne .Lstat_mode_done"); // no recognized extension + emitter.instruction("movzx eax, WORD PTR [rdi + rcx * 2 - 6]"); // first extension letter + emitter.instruction("or eax, 0x20"); // ASCII case-fold e/E, c/C, b/B + emitter.instruction("cmp eax, 101"); // .e?? (exe)? + emitter.instruction("je .Lstat_exec_e"); // inspect exe + emitter.instruction("cmp eax, 99"); // .c?? (com/cmd)? + emitter.instruction("je .Lstat_exec_c"); // inspect com/cmd + emitter.instruction("cmp eax, 98"); // .b?? (bat)? + emitter.instruction("jne .Lstat_mode_done"); // unsupported extension + emitter.instruction("movzx eax, WORD PTR [rdi + rcx * 2 - 4]"); // second extension letter + emitter.instruction("or eax, 0x20"); // ASCII case-fold a/A + emitter.instruction("cmp eax, 97"); // .ba? + emitter.instruction("jne .Lstat_mode_done"); // not .bat + emitter.instruction("movzx eax, WORD PTR [rdi + rcx * 2 - 2]"); // third extension letter + emitter.instruction("or eax, 0x20"); // ASCII case-fold t/T + emitter.instruction("cmp eax, 116"); // .bat? + emitter.instruction("jne .Lstat_mode_done"); // not .bat + emitter.instruction("or DWORD PTR [rsp + 156], 0x49"); // .bat keeps php-src's compatibility execute bits 0111 + emitter.instruction("jmp .Lstat_mode_done"); // mode is complete + emitter.label(".Lstat_exec_e"); + emitter.instruction("movzx eax, WORD PTR [rdi + rcx * 2 - 4]"); // second extension letter + emitter.instruction("or eax, 0x20"); // ASCII case-fold x/X + emitter.instruction("cmp eax, 120"); // .ex? + emitter.instruction("jne .Lstat_mode_done"); // not .exe + emitter.instruction("movzx eax, WORD PTR [rdi + rcx * 2 - 2]"); // third extension letter + emitter.instruction("or eax, 0x20"); // ASCII case-fold e/E + emitter.instruction("cmp eax, 101"); // .exe? + emitter.instruction("jne .Lstat_mode_done"); // not .exe + emitter.instruction("jmp .Lstat_exec_binary"); // validate PE/DOS executable through Win32 + emitter.label(".Lstat_exec_c"); + emitter.instruction("movzx eax, WORD PTR [rdi + rcx * 2 - 4]"); // second extension letter + emitter.instruction("or eax, 0x20"); // ASCII case-fold o/O or m/M + emitter.instruction("cmp eax, 111"); // .co? (com)? + emitter.instruction("je .Lstat_exec_com"); // inspect .com + emitter.instruction("cmp eax, 109"); // .cm? (cmd)? + emitter.instruction("jne .Lstat_mode_done"); // neither .com nor .cmd + emitter.instruction("movzx eax, WORD PTR [rdi + rcx * 2 - 2]"); // third extension letter + emitter.instruction("or eax, 0x20"); // ASCII case-fold d/D + emitter.instruction("cmp eax, 100"); // .cmd? + emitter.instruction("jne .Lstat_mode_done"); // not .cmd + emitter.instruction("or DWORD PTR [rsp + 156], 0x49"); // .cmd keeps php-src's compatibility execute bits 0111 + emitter.instruction("jmp .Lstat_mode_done"); // mode is complete + emitter.label(".Lstat_exec_com"); + emitter.instruction("movzx eax, WORD PTR [rdi + rcx * 2 - 2]"); // third extension letter + emitter.instruction("or eax, 0x20"); // ASCII case-fold m/M + emitter.instruction("cmp eax, 109"); // .com? + emitter.instruction("jne .Lstat_mode_done"); // not .com + emitter.label(".Lstat_exec_binary"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 72]"); // lpApplicationName = original UTF-16 path + emitter.instruction("lea rdx, [rsp + 152]"); // lpBinaryType = stack DWORD outside shadow space + emitter.instruction("call GetBinaryTypeW"); // validate .exe/.com as executable images + emitter.instruction("test eax, eax"); // accepted executable image? + emitter.instruction("jz .Lstat_mode_done"); // retain non-executable base mode + emitter.instruction("or DWORD PTR [rsp + 156], 0x49"); // add user/group/other execute bits + emitter.label(".Lstat_mode_done"); + emitter.instruction("mov eax, DWORD PTR [rsp + 156]"); // reload synthesized mode after optional executable probe + emitter.instruction(&format!("mov DWORD PTR [rsi + {}], eax", mode_off)); // store st_mode + emitter.instruction(&format!("mov DWORD PTR [rsi + {}], 1", nlink_off)); // temporary fallback until handle metadata supplies the real link count + // -- timestamps: convert each FILETIME to Unix epoch seconds -- + emit_filetime_to_stat_seconds(emitter, 44, atime_off); + emit_filetime_to_stat_seconds(emitter, 52, mtime_off); + emit_filetime_to_stat_seconds(emitter, 36, ctime_off); + // -- st_ino: CreateFileW + GetFileInformationByHandle synthesize a stable file id -- + emitter.instruction("mov rcx, QWORD PTR [rsp + 72]"); // lpFileName = the retained UTF-16 path + emitter.instruction("xor edx, edx"); // dwDesiredAccess = 0 (metadata-only open) + emitter.instruction("mov r8, 7"); // dwShareMode = FILE_SHARE_READ|WRITE|DELETE + emitter.instruction("xor r9, r9"); // lpSecurityAttributes = NULL + emitter.instruction("mov QWORD PTR [rsp + 32], 3"); // dwCreationDisposition = OPEN_EXISTING + emitter.instruction("mov QWORD PTR [rsp + 40], 0x2000000"); // dwFlagsAndAttributes = FILE_FLAG_BACKUP_SEMANTICS (lets directories open) + emitter.instruction("mov QWORD PTR [rsp + 48], 0"); // hTemplateFile = NULL + emitter.instruction("call CreateFileW"); // open a metadata-only handle to the Unicode path + emitter.instruction("cmp rax, -1"); // INVALID_HANDLE_VALUE? + emitter.instruction("je .Lstat_ino_skip"); // leave st_ino at 0 when the handle cannot be opened + emitter.instruction("mov QWORD PTR [rsp + 80], rax"); // save the handle across GetFileInformationByHandle + emitter.instruction("cld"); // forward direction for rep stosb + emitter.instruction("lea rdi, [rsp + 88]"); // dest = BY_HANDLE_FILE_INFORMATION buffer + emitter.instruction("xor eax, eax"); // zero fill byte + emitter.instruction("mov ecx, 52"); // sizeof(BY_HANDLE_FILE_INFORMATION) + emitter.instruction("rep stosb"); // zero the buffer before the query + emitter.instruction("mov rcx, QWORD PTR [rsp + 80]"); // hFile = the opened handle + emitter.instruction("lea rdx, [rsp + 88]"); // lpFileInformation = &BY_HANDLE_FILE_INFORMATION + emitter.instruction("call GetFileInformationByHandle"); // query followed target metadata and link count + emitter.instruction("test eax, eax"); // metadata query succeeded? + emitter.instruction("jz .Lstat_metadata_skip"); // retain attribute-query fallback when the handle rejects metadata + // The path query above reports the reparse point's own zero length under + // Wine. The ordinary handle follows a symlink, so prefer its target + // metadata for stat() just as PHP/POSIX do (lstat() corrects the type later). + emitter.instruction("mov rsi, QWORD PTR [rsp + 144]"); // restore caller stat destination + emitter.instruction("mov eax, DWORD PTR [rsp + 124]"); // followed target nFileSizeLow + emitter.instruction("mov edx, DWORD PTR [rsp + 120]"); // followed target nFileSizeHigh + emitter.instruction("shl rdx, 32"); // position target size high dword + emitter.instruction("or rax, rdx"); // combine followed target size + emitter.instruction(&format!("mov QWORD PTR [rsi + {}], rax", size_off)); // publish followed target size + emitter.instruction("mov eax, DWORD PTR [rsp + 116]"); // dwVolumeSerialNumber identifies the backing volume + emitter.instruction("test eax, eax"); // Wine may expose a zero serial for synthetic volumes + emitter.instruction("jnz .Lstat_dev_ready"); // retain a real nonzero serial + emitter.instruction("mov eax, 1"); // stable nonzero synthetic device id + emitter.label(".Lstat_dev_ready"); + emitter.instruction(&format!("mov QWORD PTR [rsi + {}], rax", dev_off)); // publish st_dev for linkinfo/stat arrays + emitter.instruction("mov eax, DWORD PTR [rsp + 132]"); // nFileIndexHigh (base+44, base = rsp+88) + emitter.instruction("shl rax, 32"); // shift into the upper 32 bits of the 64-bit id + emitter.instruction("mov ecx, DWORD PTR [rsp + 136]"); // nFileIndexLow (base+48, base = rsp+88) + emitter.instruction("or rax, rcx"); // combine into the full 64-bit inode number + emitter.instruction(&format!("mov QWORD PTR [rsi + {}], rax", ino_off)); // store st_ino + emitter.instruction("mov eax, DWORD PTR [rsp + 128]"); // nNumberOfLinks (base+40, base = rsp+88) + emitter.instruction(&format!("mov DWORD PTR [rsi + {}], eax", nlink_off)); // publish the native hard-link count + // Prefer the followed handle's attributes and timestamps over the path + // query. This matters for symlinks, whose link attributes can differ from + // the target metadata returned by PHP's ordinary stat(). + emitter.instruction("mov edx, DWORD PTR [rsp + 88]"); // followed target dwFileAttributes + emitter.instruction("mov ecx, DWORD PTR [rsp + 156]"); // retain executable bits derived from the original path suffix + emitter.instruction("and ecx, 0x49"); // isolate user/group/other execute bits + emitter.instruction("mov eax, 0x81B6"); // followed regular-file mode = S_IFREG | 0666 + emitter.instruction("test edx, 0x10"); // followed target is a directory? + emitter.instruction("jz .Lstat_handle_mode_readonly"); // regular files retain the suffix-derived execute bits + emitter.instruction("mov eax, 0x41FF"); // followed directory mode = S_IFDIR | 0777 + emitter.label(".Lstat_handle_mode_readonly"); + emitter.instruction("test edx, 1"); // followed target is readonly? + emitter.instruction("jz .Lstat_handle_mode_ready"); // writable target retains write bits + emitter.instruction("and eax, 0xFF6D"); // readonly target clears 0222 + emitter.label(".Lstat_handle_mode_ready"); + emitter.instruction("or eax, ecx"); // restore valid .exe/.com/.bat/.cmd execute bits + emitter.instruction(&format!("mov DWORD PTR [rsi + {}], eax", mode_off)); // publish followed target type and permissions + emit_filetime_to_stat_seconds(emitter, 100, atime_off); + emit_filetime_to_stat_seconds(emitter, 108, mtime_off); + emit_filetime_to_stat_seconds(emitter, 92, ctime_off); + emitter.instruction("mov rcx, QWORD PTR [rsp + 80]"); // hFile for Win32 object-type classification + emitter.instruction("call GetFileType"); // distinguish named pipes and character devices from files + emitter.instruction("cmp eax, 3"); // FILE_TYPE_PIPE? + emitter.instruction("je .Lstat_pipe_mode"); // replace the regular-file type with S_IFIFO + emitter.instruction("cmp eax, 2"); // FILE_TYPE_CHAR? + emitter.instruction("jne .Lstat_metadata_skip"); // files/directories retain their path-derived mode + emitter.instruction("mov eax, 0x21B6"); // st_mode = S_IFCHR | 0666 + emitter.instruction("mov edx, DWORD PTR [rsp + 88]"); // dwFileAttributes from BY_HANDLE_FILE_INFORMATION + emitter.instruction("test edx, 1"); // FILE_ATTRIBUTE_READONLY? + emitter.instruction("jz .Lstat_special_mode_ready"); // writable character device + emitter.instruction("and eax, 0xFF6D"); // readonly character device clears 0222 + emitter.instruction("jmp .Lstat_special_mode_ready"); // store the selected character mode + emitter.label(".Lstat_pipe_mode"); + emitter.instruction("mov eax, 0x11B6"); // st_mode = S_IFIFO | 0666 + emitter.instruction("mov edx, DWORD PTR [rsp + 88]"); // dwFileAttributes from BY_HANDLE_FILE_INFORMATION + emitter.instruction("test edx, 1"); // FILE_ATTRIBUTE_READONLY? + emitter.instruction("jz .Lstat_special_mode_ready"); // writable pipe + emitter.instruction("and eax, 0xFF6D"); // readonly pipe clears 0222 + emitter.label(".Lstat_special_mode_ready"); + emitter.instruction(&format!("mov DWORD PTR [rsi + {}], eax", mode_off)); // publish pipe/character object type and permissions + emitter.label(".Lstat_metadata_skip"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 80]"); // reload the handle + emitter.instruction("call CloseHandle"); // release the metadata-only handle + emitter.label(".Lstat_ino_skip"); + emitter.instruction("mov rax, QWORD PTR [rsp + 72]"); // owned wide path + emitter.instruction("call __rt_heap_free"); // release the converted path after both queries + emitter.instruction("xor eax, eax"); // return 0 (success) + emitter.instruction("add rsp, 168"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lstat_fail"); + emitter.instruction("call GetLastError"); // capture query failure before cleanup + emitter.instruction("mov DWORD PTR [rsp + 80], eax"); // preserve native error code + emitter.instruction("mov rax, QWORD PTR [rsp + 72]"); // owned wide path + emitter.instruction("call __rt_heap_free"); // release the converted path on failure + emitter.instruction("mov eax, DWORD PTR [rsp + 80]"); // restore native error code + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // retain native Win32 state + emitter.instruction("call __rt_win32_errno_from_code"); // translate to POSIX errno + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish observable errno + emitter.instruction("mov rax, -1"); // return -1 on failure + emitter.instruction("add rsp, 168"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lstat_conversion_fail"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 84"); // EILSEQ: invalid UTF-8 path + emitter.instruction("mov rax, -1"); // return -1 on failure + emitter.instruction("add rsp, 168"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits the conversion of a Win32 `FILETIME` (100ns ticks since 1601) held at +/// `[rsp + src_off]` into Unix epoch seconds stored at `[rsi + dst_off]` of the +/// Linux-layout stat buffer. Clobbers rax/rdx/r8/r9 (all MSx64 volatile) and +/// leaves rsi (the stat buffer base) intact. +fn emit_filetime_to_stat_seconds(emitter: &mut Emitter, src_off: usize, dst_off: usize) { + emitter.instruction(&format!("mov rax, QWORD PTR [rsp + {}]", src_off)); // load the 64-bit FILETIME + emitter.instruction("mov r8, 116444736000000000"); // 1601->1970 offset in 100ns units + emitter.instruction("sub rax, r8"); // rebase the tick count onto the Unix epoch + emitter.instruction("cqo"); // sign-extend negative pre-1970 FILETIME deltas + emitter.instruction("mov r9, 10000000"); // 100ns intervals per second + emitter.instruction("idiv r9"); // rax = signed whole seconds since 1970 + emitter.instruction(&format!("mov QWORD PTR [rsi + {}], rax", dst_off)); // store the timestamp seconds +} + +/// Emits a shim that wraps `MoveFileExW` for `rename`, translating the Win32 +/// `BOOL` result (nonzero = success) to the POSIX convention `__rt_rename` +/// expects (0 = success, -1 = failure). Without the translation a successful +/// rename (`BOOL` nonzero, compared against 0 by `__rt_rename`) would be +/// reported as failure and vice versa — mirrors the `link` shim. +/// `MOVEFILE_REPLACE_EXISTING` matches php-src's write-then-rename overwrite. +pub(super) fn emit_shim_rename(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_rename"); + emitter.instruction("sub rsp, 72"); // shadow space and aligned two-path ownership locals + emitter.instruction("mov QWORD PTR [rsp + 32], rsi"); // preserve UTF-8 destination across conversion + emitter.instruction("call __rt_win_utf8_to_utf16"); // convert UTF-8 source path + emitter.instruction("test rax, rax"); // source conversion succeeded? + emitter.instruction("jz .Lrename_conversion_fail"); // invalid UTF-8 source + emitter.instruction("mov QWORD PTR [rsp + 40], rax"); // retain owned wide source + emitter.instruction("mov rdi, QWORD PTR [rsp + 32]"); // UTF-8 destination path + emitter.instruction("call __rt_win_utf8_to_utf16"); // convert UTF-8 destination path + emitter.instruction("test rax, rax"); // destination conversion succeeded? + emitter.instruction("jz .Lrename_destination_conversion_fail"); // release source before returning + emitter.instruction("mov QWORD PTR [rsp + 48], rax"); // retain owned wide destination + emitter.instruction("mov rcx, QWORD PTR [rsp + 40]"); // lpExistingFileName = wide source + emitter.instruction("mov rdx, rax"); // lpNewFileName = wide destination + emitter.instruction("mov r8d, 3"); // MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED + emitter.instruction("call MoveFileExW"); // rename Unicode path, replacing existing target + emitter.instruction("test eax, eax"); // Win32 BOOL: nonzero = success + emitter.instruction("jz .Lrename_fail"); // zero = failure + emitter.instruction("mov DWORD PTR [rsp + 56], 0"); // preserve POSIX success across cleanup + emitter.instruction("jmp .Lrename_cleanup"); // release both converted paths + emitter.label(".Lrename_fail"); + emitter.instruction("call GetLastError"); // capture native rename failure + emitter.instruction("mov DWORD PTR [rsp + 56], eax"); // preserve native error across cleanup + emitter.label(".Lrename_cleanup"); + emitter.instruction("mov rax, QWORD PTR [rsp + 48]"); // owned wide destination + emitter.instruction("call __rt_heap_free"); // release destination + emitter.instruction("mov rax, QWORD PTR [rsp + 40]"); // owned wide source + emitter.instruction("call __rt_heap_free"); // release source + emitter.instruction("mov eax, DWORD PTR [rsp + 56]"); // zero success or native error + emitter.instruction("test eax, eax"); // was the operation successful? + emitter.instruction("jz .Lrename_success"); // return POSIX zero + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // retain native Win32 state + emitter.instruction("call __rt_win32_errno_from_code"); // translate failure + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish errno + emitter.instruction("mov rax, -1"); // POSIX rename failure + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lrename_success"); + emitter.instruction("xor eax, eax"); // POSIX rename success + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lrename_destination_conversion_fail"); + emitter.instruction("mov rax, QWORD PTR [rsp + 40]"); // owned wide source + emitter.instruction("call __rt_heap_free"); // release source after destination conversion failure + emitter.label(".Lrename_conversion_fail"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 84"); // EILSEQ: invalid UTF-8 path + emitter.instruction("mov rax, -1"); // POSIX rename failure + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits a shim that wraps `SetFileAttributesW` for `chmod`. +/// +/// SysV: rdi=path, rsi=mode. +/// Maps Unix mode to Win32 file attributes: +/// - If mode has no write bits (mode & 0222 == 0) → FILE_ATTRIBUTE_READONLY (1) +/// - Otherwise → FILE_ATTRIBUTE_NORMAL (128) +pub(super) fn emit_shim_chmod(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_chmod"); + emitter.instruction("sub rsp, 56"); // shadow space and aligned owned-path locals + emitter.instruction("mov QWORD PTR [rsp + 40], rsi"); // preserve mode across conversion + emitter.instruction("call __rt_win_utf8_to_utf16"); // convert path strictly to UTF-16 + emitter.instruction("test rax, rax"); // conversion succeeded? + emitter.instruction("jz .Lchmod_conversion_fail"); // invalid UTF-8 path + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // retain owned wide path + emitter.instruction("mov rcx, rax"); // native path + emitter.instruction("mov rsi, QWORD PTR [rsp + 40]"); // restore Unix mode + emitter.instruction("test rsi, 0222"); // any write bit set? + emitter.instruction("jnz .Lchmod_writable"); // → FILE_ATTRIBUTE_NORMAL + emitter.instruction("mov rdx, 1"); // FILE_ATTRIBUTE_READONLY + emitter.instruction("jmp .Lchmod_call"); // → call SetFileAttributesW + emitter.label(".Lchmod_writable"); + emitter.instruction("mov rdx, 128"); // FILE_ATTRIBUTE_NORMAL + emitter.label(".Lchmod_call"); + emitter.instruction("call SetFileAttributesW"); // set Unicode file attributes + emitter.instruction("test eax, eax"); // Win32 BOOL success? + emitter.instruction("jz .Lchmod_fail"); // capture native failure + emitter.instruction("mov DWORD PTR [rsp + 40], 0"); // preserve POSIX success across cleanup + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // owned wide path + emitter.instruction("call __rt_heap_free"); // release path + emitter.instruction("mov eax, DWORD PTR [rsp + 40]"); // restore POSIX success + emitter.instruction("add rsp, 56"); // restore stack + emitter.instruction("ret"); // return success + emitter.label(".Lchmod_fail"); + emitter.instruction("call GetLastError"); // capture native failure + emitter.instruction("mov DWORD PTR [rsp + 40], eax"); // preserve error across cleanup + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // owned wide path + emitter.instruction("call __rt_heap_free"); // release path + emitter.instruction("mov eax, DWORD PTR [rsp + 40]"); // reload native error + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // retain native state + emitter.instruction("call __rt_win32_errno_from_code"); // translate failure + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish errno + emitter.instruction("mov eax, -1"); // POSIX chmod failure + emitter.instruction("add rsp, 56"); // restore stack + emitter.instruction("ret"); // return failure + emitter.label(".Lchmod_conversion_fail"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 84"); // EILSEQ: invalid UTF-8 path + emitter.instruction("mov eax, -1"); // POSIX chmod failure + emitter.instruction("add rsp, 56"); // restore stack + emitter.instruction("ret"); // return failure + emitter.blank(); +} + +/// Emits the Windows ACL helpers that back php-parity `is_readable()` / +/// `is_writable()`. +/// +/// Must be emitted before [`emit_shim_access`]: its `.Laccess_acl_gate` calls +/// `__rt_win_acl_grants`. +pub(super) fn emit_win_acl_helpers(emitter: &mut Emitter) { + emit_win_acl_token_get(emitter); + emit_win_acl_grants(emitter); +} + +/// Emits `__rt_win_acl_token_get() -> HANDLE`, the cached impersonation +/// token php's `tsrm_win32_access` keeps in `TWG(impersonation_token)`. +/// +/// SysV: no arguments. Returns the borrowed token in rax, or NULL when no +/// token could be obtained. Ownership stays with the process-wide cache: the +/// handle is opened at most once and released by process teardown, so no +/// caller ever closes it and no path can close it twice. +/// +/// `_win_acl_token` is tri-state: 0 = never attempted, -1 = attempted and +/// permanently unavailable, anything else = the cached token. A real token +/// handle can never be -1, so the sentinel is unambiguous. Caching the +/// failure is deliberate: a restricted token (low integrity, AppContainer, a +/// service token) fails the dance on every call, and retrying it per +/// `is_readable()` would pay two Win32 calls for a verdict that cannot +/// change. A cached -1 simply leaves every later call on the +/// `GetFileAttributesW` answer, which is exactly today's behavior. +/// +/// php requests `TOKEN_ALL_ACCESS`; this asks only for `TOKEN_QUERY`, the +/// single right `AccessCheck` documents as required. That narrower mask is +/// the one deliberate divergence: it succeeds under restricted tokens that +/// refuse `TOKEN_ALL_ACCESS`, and when both succeed the verdict is identical. +/// +/// A thread that was already impersonating (a SAPI host) is left exactly as +/// found. Only an impersonation this helper starts is undone with +/// `RevertToSelf`; the opened token object stays valid afterwards, which is +/// what lets the handle be cached instead of leaving the process +/// impersonating for the rest of its life the way php-src does. +/// +/// Frame: `sub rsp, 56` — 32 bytes of MSx64 shadow space plus the +/// `OpenThreadToken` out-param (+32) and the "this call started the +/// impersonation" flag (+40). 56 mod 16 == 8, so the entry `rsp` (congruent +/// to 8 after the caller's `call` pushed a return address onto a 16-byte +/// aligned stack) becomes 16-byte aligned for every nested call. +fn emit_win_acl_token_get(emitter: &mut Emitter) { + emitter.label_global("__rt_win_acl_token_get"); + emitter.instruction("mov rax, QWORD PTR [rip + _win_acl_token]"); // cached impersonation token, php's TWG(impersonation_token) + emitter.instruction("test rax, rax"); // has the token dance ever run? + emitter.instruction("jz .Lacl_token_acquire"); // first call performs it + emitter.instruction("cmp rax, -1"); // did an earlier attempt fail permanently? + emitter.instruction("je .Lacl_token_none"); // never retry: stay on the attribute-only answer + emitter.instruction("ret"); // hand back the cached token without building a frame + emitter.label(".Lacl_token_none"); + emitter.instruction("xor eax, eax"); // NULL tells the caller to fail open + emitter.instruction("ret"); // return + emitter.label(".Lacl_token_acquire"); + emitter.instruction("sub rsp, 56"); // shadow space plus the token and impersonation-flag locals + emitter.instruction("mov QWORD PTR [rsp + 32], 0"); // OpenThreadToken out-param starts empty + emitter.instruction("mov QWORD PTR [rsp + 40], 0"); // no impersonation has been started by this call + emitter.instruction("call GetCurrentThread"); // pseudo-handle for the calling thread + emitter.instruction("mov rcx, rax"); // ThreadHandle + emitter.instruction("mov edx, 8"); // TOKEN_QUERY: the only right AccessCheck requires + emitter.instruction("mov r8d, 1"); // OpenAsSelf = TRUE, as php passes + emitter.instruction("lea r9, [rsp + 32]"); // &TokenHandle + emitter.instruction("call OpenThreadToken"); // reuse a token the host already impersonates + emitter.instruction("test eax, eax"); // did the thread already carry one? + emitter.instruction("jnz .Lacl_token_have"); // nothing to impersonate + emitter.instruction("mov ecx, 2"); // SecurityImpersonation + emitter.instruction("call ImpersonateSelf"); // php's ERROR_NO_TOKEN recovery + emitter.instruction("test eax, eax"); // impersonation refused by a restricted token? + emitter.instruction("jz .Lacl_token_fail"); // remember the failure and fail open + emitter.instruction("mov QWORD PTR [rsp + 40], 1"); // this call now owes a RevertToSelf + emitter.instruction("call GetCurrentThread"); // pseudo-handle again after the Win32 call + emitter.instruction("mov rcx, rax"); // ThreadHandle + emitter.instruction("mov edx, 8"); // TOKEN_QUERY + emitter.instruction("mov r8d, 1"); // OpenAsSelf = TRUE + emitter.instruction("lea r9, [rsp + 32]"); // &TokenHandle + emitter.instruction("call OpenThreadToken"); // open the token the impersonation just attached + emitter.instruction("test eax, eax"); // still no token? + emitter.instruction("jz .Lacl_token_fail"); // remember the failure and fail open + emitter.label(".Lacl_token_have"); + emitter.instruction("cmp QWORD PTR [rsp + 40], 0"); // did this call start the impersonation? + emitter.instruction("je .Lacl_token_publish"); // a host impersonation must survive untouched + emitter.instruction("call RevertToSelf"); // detach it; the opened token object stays valid + emitter.label(".Lacl_token_publish"); + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // the opened impersonation token + emitter.instruction("test rax, rax"); // defensive: a TRUE return with a NULL handle + emitter.instruction("jz .Lacl_token_reverted"); // treat it as a permanent failure + emitter.instruction("mov QWORD PTR [rip + _win_acl_token], rax"); // cache it for every later access() call + emitter.instruction("add rsp, 56"); // release the frame + emitter.instruction("ret"); // return the borrowed token + emitter.label(".Lacl_token_fail"); + emitter.instruction("cmp QWORD PTR [rsp + 40], 0"); // did this call start the impersonation? + emitter.instruction("je .Lacl_token_reverted"); // nothing to undo + emitter.instruction("call RevertToSelf"); // leave the thread exactly as it was found + emitter.label(".Lacl_token_reverted"); + emitter.instruction("mov QWORD PTR [rip + _win_acl_token], -1"); // remember the permanent failure + emitter.instruction("xor eax, eax"); // NULL tells the caller to fail open + emitter.instruction("add rsp, 56"); // release the frame + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits `__rt_win_acl_grants(const WCHAR *path, DWORD desired) -> i64`, the +/// `GetFileSecurityW` + `AccessCheck` verdict php's `tsrm_win32_access` uses +/// for `R_OK`/`W_OK` on Windows. +/// +/// SysV: rdi = borrowed UTF-16 path (the caller keeps ownership), esi = +/// desired access mask. Returns 1 when the DACL grants the request, 0 when it +/// denies it, and -1 when the check could not be performed at all. +/// +/// -1 is the whole point of the helper: every failure — unsupported volume, +/// missing path, a descriptor too large for the frame's buffer, no +/// impersonation token, a failing `AccessCheck` — returns -1 so +/// `__rt_sys_access` keeps the `GetFileAttributesW` answer it already +/// computed. A denial is only ever reported when `AccessCheck` itself +/// succeeded and set `AccessStatus` to FALSE, so the worst case of this whole +/// path is exactly today's behavior. +/// +/// UNC paths are refused up front: `GetFileSecurityW` returns the *remote* +/// descriptor while `AccessCheck` evaluates it against a *local* token, which +/// produces confident wrong denials on domain shares. php-src has that wart; +/// this helper fails open on `\\server\share` instead of inheriting it. +/// +/// The helper owns no resources. The security descriptor lives in a +/// fixed 2048-byte buffer inside this frame, so there is nothing to release +/// on any path and no allocator to fail: a descriptor whose probed size +/// exceeds the buffer simply takes the fail-open exit. (An earlier draft +/// called `__rt_heap_alloc` and treated a NULL return as "fail open", but +/// that helper never returns NULL — it diverges through +/// `__rt_heap_exhausted` — so the guard was unreachable and exhaustion was +/// undefined control flow inside a Win32 shim.) The token is borrowed from +/// `__rt_win_acl_token_get`'s process-wide cache and is never closed here. +/// +/// Frame: `sub rsp, 2232`, laid out as +/// `[rsp+0..31]` MSx64 shadow space, `[rsp+32..63]` the four stack arguments +/// `AccessCheck` needs beyond rcx/rdx/r8/r9, `[rsp+64]` lpFileName, +/// `[rsp+72]` the desired access mask `MapGenericMask` rewrites in place, +/// `[rsp+80]` the borrowed token, `[rsp+88]`/`[rsp+92]` lpnLengthNeeded and +/// GrantedAccess, `[rsp+96]`/`[rsp+100]` AccessStatus and PrivilegeSetLength, +/// `[rsp+104..151]` the 48-byte `PRIVILEGE_SET`, `[rsp+152..167]` the +/// `GENERIC_MAPPING`, `[rsp+168..175]` slack that lands the descriptor buffer +/// on a 16-byte boundary, `[rsp+176..2223]` the 2048-byte security +/// descriptor, and `[rsp+2224..2231]` the tail slack that makes the total +/// 2232 = 139 * 16 + 8. That residue of 8 is the alignment contract: entry +/// `rsp` is congruent to 8 mod 16 (the caller's `call` pushed a return +/// address onto a 16-byte aligned stack), so subtracting a size congruent to +/// 8 leaves `rsp` 16-byte aligned at every nested `call`, as MSx64 requires. +/// 2232 also stays under a 4 KiB page, so no `__chkstk` probe is needed to +/// step past the stack guard page. +fn emit_win_acl_grants(emitter: &mut Emitter) { + emitter.label_global("__rt_win_acl_grants"); + emitter.instruction("sub rsp, 2232"); // shadow space, stack arguments, ACL locals, descriptor + emitter.instruction("mov QWORD PTR [rsp + 64], rdi"); // borrowed UTF-16 path + emitter.instruction("mov eax, esi"); // zero-extend the requested access mask + emitter.instruction("mov QWORD PTR [rsp + 72], rax"); // MapGenericMask reads and rewrites this DWORD + emitter.instruction("mov QWORD PTR [rsp + 80], 0"); // no impersonation token is borrowed yet + emitter.instruction("mov QWORD PTR [rsp + 88], 0"); // lpnLengthNeeded and GrantedAccess out-params + emitter.instruction("mov QWORD PTR [rsp + 96], 0"); // AccessStatus and PrivilegeSetLength out-params + emitter.instruction("mov QWORD PTR [rsp + 104], 0"); // PRIVILEGE_SET, zeroed exactly like php's `= {0}` + emitter.instruction("mov QWORD PTR [rsp + 112], 0"); // PRIVILEGE_SET + emitter.instruction("mov QWORD PTR [rsp + 120], 0"); // PRIVILEGE_SET + emitter.instruction("mov QWORD PTR [rsp + 128], 0"); // PRIVILEGE_SET + emitter.instruction("mov QWORD PTR [rsp + 136], 0"); // PRIVILEGE_SET + emitter.instruction("mov QWORD PTR [rsp + 144], 0"); // PRIVILEGE_SET tail + emitter.instruction("mov DWORD PTR [rsp + 152], 0x00120089"); // GENERIC_MAPPING.GenericRead = FILE_GENERIC_READ + emitter.instruction("mov DWORD PTR [rsp + 156], 0x00120116"); // GENERIC_MAPPING.GenericWrite = FILE_GENERIC_WRITE + emitter.instruction("mov DWORD PTR [rsp + 160], 0x001200A0"); // GENERIC_MAPPING.GenericExecute = FILE_GENERIC_EXEC + emitter.instruction("mov DWORD PTR [rsp + 164], 0x001F01FF"); // GENERIC_MAPPING.GenericAll = FILE_ALL_ACCESS + emitter.instruction("movzx ecx, WORD PTR [rdi]"); // first UTF-16 code unit of the path + emitter.instruction("cmp ecx, 0x5C"); // leading backslash? + emitter.instruction("je .Lacl_unc_probe"); // may begin a UNC prefix + emitter.instruction("cmp ecx, 0x2F"); // leading forward slash? + emitter.instruction("jne .Lacl_local_path"); // ordinary local path + emitter.label(".Lacl_unc_probe"); + emitter.instruction("movzx ecx, WORD PTR [rdi + 2]"); // second code unit; the NUL terminator at worst + emitter.instruction("cmp ecx, 0x5C"); // a doubled backslash opens a UNC share + emitter.instruction("je .Lacl_unavailable"); // a remote DACL cannot be judged by a local token + emitter.instruction("cmp ecx, 0x2F"); // a doubled forward slash opens the same share + emitter.instruction("je .Lacl_unavailable"); // fail open: the attribute answer is the safer one + emitter.label(".Lacl_local_path"); + emitter.instruction("mov rcx, rdi"); // lpFileName + emitter.instruction("mov edx, 7"); // OWNER | GROUP | DACL_SECURITY_INFORMATION + emitter.instruction("xor r8d, r8d"); // pSecurityDescriptor = NULL for the size probe + emitter.instruction("xor r9d, r9d"); // nLength = 0 + emitter.instruction("lea rax, [rsp + 88]"); // &lpnLengthNeeded + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // fifth argument travels on the stack + emitter.instruction("call GetFileSecurityW"); // php's first call: probe the descriptor size + emitter.instruction("mov eax, DWORD PTR [rsp + 88]"); // required byte count, still zero when unreported + emitter.instruction("test eax, eax"); // did the probe report a size? + emitter.instruction("jz .Lacl_unavailable"); // unsupported volume or missing path: fail open + emitter.instruction("cmp eax, 2048"); // does the descriptor fit the frame's fixed buffer? + emitter.instruction("ja .Lacl_unavailable"); // oversized descriptor: fail open, never allocate + emitter.instruction("mov rcx, QWORD PTR [rsp + 64]"); // lpFileName + emitter.instruction("mov edx, 7"); // OWNER | GROUP | DACL_SECURITY_INFORMATION + emitter.instruction("lea r8, [rsp + 176]"); // pSecurityDescriptor = the frame's fixed buffer + emitter.instruction("mov r9d, 2048"); // nLength = that buffer's real capacity + emitter.instruction("lea rax, [rsp + 88]"); // &lpnLengthNeeded + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // fifth argument travels on the stack + emitter.instruction("call GetFileSecurityW"); // php's second call: fill the descriptor + emitter.instruction("test eax, eax"); // did the descriptor read succeed? + emitter.instruction("jz .Lacl_unavailable"); // fail open on a refused descriptor + emitter.instruction("call __rt_win_acl_token_get"); // borrow php's cached impersonation token + emitter.instruction("test rax, rax"); // is a usable token available? + emitter.instruction("jz .Lacl_unavailable"); // fail open with no token to check against + emitter.instruction("mov QWORD PTR [rsp + 80], rax"); // borrowed: the cache keeps ownership + emitter.instruction("lea rcx, [rsp + 72]"); // &desired access mask + emitter.instruction("lea rdx, [rsp + 152]"); // &GENERIC_MAPPING + emitter.instruction("call MapGenericMask"); // resolve generic bits exactly as php does + emitter.instruction("mov DWORD PTR [rsp + 100], 48"); // PrivilegeSetLength = the reserved buffer size + emitter.instruction("lea rcx, [rsp + 176]"); // pSecurityDescriptor + emitter.instruction("mov rdx, QWORD PTR [rsp + 80]"); // ClientToken + emitter.instruction("mov r8d, DWORD PTR [rsp + 72]"); // DesiredAccess after generic mapping + emitter.instruction("lea r9, [rsp + 152]"); // GenericMapping + emitter.instruction("lea rax, [rsp + 104]"); // PrivilegeSet + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // fifth argument + emitter.instruction("lea rax, [rsp + 100]"); // PrivilegeSetLength + emitter.instruction("mov QWORD PTR [rsp + 40], rax"); // sixth argument + emitter.instruction("lea rax, [rsp + 92]"); // GrantedAccess + emitter.instruction("mov QWORD PTR [rsp + 48], rax"); // seventh argument + emitter.instruction("lea rax, [rsp + 96]"); // AccessStatus + emitter.instruction("mov QWORD PTR [rsp + 56], rax"); // eighth argument + emitter.instruction("call AccessCheck"); // php's DACL verdict for this token + emitter.instruction("test eax, eax"); // did AccessCheck itself succeed? + emitter.instruction("jz .Lacl_unavailable"); // fail open on a check that could not run + emitter.instruction("cmp DWORD PTR [rsp + 96], 0"); // AccessStatus: FALSE means the DACL denies it + emitter.instruction("setne al"); // 1 = granted, 0 = denied + emitter.instruction("movzx eax, al"); // widen the verdict; the zero-extend clears rax + emitter.instruction("add rsp, 2232"); // release the frame + emitter.instruction("ret"); // return 1 (granted) or 0 (denied) + emitter.label(".Lacl_unavailable"); + emitter.instruction("mov rax, -1"); // the check could not run: the caller keeps its answer + emitter.instruction("add rsp, 2232"); // release the frame + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits a shim that converts `access(path, mode)` to `GetFileAttributesW`. +/// +/// SysV: rdi=path, rsi=mode. `GetFileAttributesW` performs the common existence +/// check; PHP's Windows rules then reject `W_OK` for a readonly path and use +/// `GetBinaryTypeW` to decide `X_OK`. +/// +/// `R_OK`/`W_OK` that survive the attribute rules are then handed to +/// `__rt_win_acl_grants`, php-src's `GetFileSecurityW` + `AccessCheck` pair +/// (TSRM/tsrm_win32.c). That check can only TIGHTEN a positive attribute +/// answer: it returns -1 whenever it cannot run, and the gate treats -1 the +/// same as "granted", so the worst case of the whole ACL path is the +/// attribute-only behavior this shim had before. `X_OK` never reaches the +/// gate — `GetBinaryTypeW` already answers it exactly as php does. +/// `GetFileAttributesW` returns `INVALID_FILE_ATTRIBUTES` (0xFFFFFFFF) when +/// the path does not resolve. +pub(super) fn emit_shim_access(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_access"); + emitter.instruction("sub rsp, 72"); // shadow space, mode/error locals, and aligned owned path storage + emitter.instruction("mov QWORD PTR [rsp + 40], rsi"); // preserve requested access bits across conversion + emitter.instruction("call __rt_win_utf8_to_utf16"); // convert path strictly to UTF-16 + emitter.instruction("test rax, rax"); // conversion succeeded? + emitter.instruction("jz .Laccess_conversion_fail"); // invalid UTF-8 path + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // retain owned wide path + emitter.instruction("mov edx, DWORD PTR [rsp + 40]"); // reload F_OK/R_OK/W_OK/X_OK request bits + emitter.instruction("test edx, 1"); // X_OK requested? + emitter.instruction("jnz .Laccess_check_execute"); // php-src tests executable images before every attribute permission check + emitter.instruction("mov rcx, rax"); // lpFileName = native path + emitter.instruction("call GetFileAttributesW"); // query Unicode file attributes + emitter.instruction("cmp eax, 0xFFFFFFFF"); // INVALID_FILE_ATTRIBUTES? + emitter.instruction("je .Laccess_fail"); // → file does not exist + emitter.instruction("mov edx, DWORD PTR [rsp + 40]"); // native call clobbered mode register; reload requested access bits + emitter.instruction("test edx, 2"); // W_OK requested? + emitter.instruction("jz .Laccess_acl_gate"); // F_OK/R_OK: the DACL still owes php its verdict + emitter.instruction("test eax, 1"); // FILE_ATTRIBUTE_READONLY set? + emitter.instruction("jnz .Laccess_eacces"); // PHP exposes readonly files as not writable + emitter.instruction("jmp .Laccess_acl_gate"); // writable attributes still owe php its DACL verdict + emitter.label(".Laccess_check_execute"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 32]"); // lpApplicationName = retained UTF-16 path + emitter.instruction("lea rdx, [rsp + 52]"); // lpBinaryType = caller-owned DWORD local + emitter.instruction("call GetBinaryTypeW"); // PHP's Windows executable-file check + emitter.instruction("test eax, eax"); // recognized executable image? + emitter.instruction("jz .Laccess_fail"); // preserve GetBinaryTypeW failure + emitter.label(".Laccess_success"); + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // owned wide path + emitter.instruction("call __rt_heap_free"); // release converted path + emitter.instruction("xor eax, eax"); // return 0 for the requested supported access mode + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Laccess_eacces"); + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // owned wide path + emitter.instruction("call __rt_heap_free"); // release converted path before the synthetic failure + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 13"); // EACCES: a readonly attribute or a denying DACL + emitter.instruction("mov eax, -1"); // report POSIX access failure + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Laccess_fail"); + emitter.instruction("call GetLastError"); // fetch native failure before cleanup + emitter.instruction("mov DWORD PTR [rsp + 56], eax"); // preserve native error across cleanup + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // owned wide path + emitter.instruction("call __rt_heap_free"); // release converted path + emitter.instruction("mov eax, DWORD PTR [rsp + 56]"); // reload native error + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // retain Win32 error + emitter.instruction("call __rt_win32_errno_from_code"); // translate native error + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish errno + emitter.instruction("mov eax, -1"); // return -1 (path not found) + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Laccess_conversion_fail"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 84"); // EILSEQ: invalid UTF-8 path + emitter.instruction("mov eax, -1"); // access failure + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Laccess_acl_gate"); + emitter.instruction("mov edx, DWORD PTR [rsp + 40]"); // reload the requested access bits + emitter.instruction("xor esi, esi"); // php starts from desired_access = 0 + emitter.instruction("test edx, 4"); // R_OK requested? + emitter.instruction("jz .Laccess_acl_write"); // no read right to demand + emitter.instruction("mov esi, 0x00120089"); // FILE_GENERIC_READ, exactly php's mask + emitter.label(".Laccess_acl_write"); + emitter.instruction("test edx, 2"); // W_OK requested? + emitter.instruction("jz .Laccess_acl_ready"); // no write right to demand + emitter.instruction("or esi, 0x00120116"); // FILE_GENERIC_WRITE, exactly php's mask + emitter.label(".Laccess_acl_ready"); + emitter.instruction("test esi, esi"); // did the request name an ACL-checkable right? + emitter.instruction("jz .Laccess_success"); // F_OK: the existence probe already answered it + emitter.instruction("mov rdi, QWORD PTR [rsp + 32]"); // borrowed UTF-16 path + emitter.instruction("call __rt_win_acl_grants"); // php's GetFileSecurityW + AccessCheck verdict + emitter.instruction("test rax, rax"); // zero means the DACL denies the request + emitter.instruction("jz .Laccess_eacces"); // php reports a denial as a failed access() + emitter.instruction("jmp .Laccess_success"); // granted, or unavailable: keep the attribute answer + emitter.blank(); +} + +/// Emits a shim that converts `ftruncate(fd, length)` to `SetFilePointerEx` + `SetEndOfFile`. +/// +/// SysV: rdi=fd, rsi=length. Converts the CRT descriptor to its Win32 handle, +/// saves the current file position, seeks to `length`, calls `SetEndOfFile`, and +/// restores the original position. Returns 0 on success, or publishes errno and +/// returns -1 on failure (matching libc `ftruncate` and PHP's Windows stream path). +pub(super) fn emit_shim_ftruncate(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_ftruncate"); + // -- stack frame: shadow(32) + length/handle/old-position locals, aligned -- + emitter.instruction("sub rsp, 72"); // reserve shadow space and aligned locals + emitter.instruction("test rsi, rsi"); // reject negative file sizes before calling Win32 + emitter.instruction("js .Lftruncate_invalid_size"); // negative lengths map to EINVAL + emitter.instruction("mov QWORD PTR [rsp + 32], rsi"); // preserve the requested length across calls + emitter.instruction("call __rt_fd_to_handle"); // convert the CRT descriptor in rdi to a Win32 HANDLE + emitter.instruction("cmp rax, -1"); // INVALID_HANDLE_VALUE? + emitter.instruction("je .Lftruncate_bad_fd"); // invalid CRT descriptor maps to EBADF + emitter.instruction("cmp rax, -2"); // detached standard descriptor sentinel? + emitter.instruction("je .Lftruncate_bad_fd"); // descriptors without a HANDLE map to EBADF + emitter.instruction("mov QWORD PTR [rsp + 40], rax"); // retain the HANDLE across Win32 calls + // -- save the current file pointer before moving to the new end -- + emitter.instruction("mov rcx, rax"); // hFile = converted HANDLE + emitter.instruction("xor edx, edx"); // liDistanceToMove = 0 + emitter.instruction("lea r8, [rsp + 48]"); // lpNewFilePointer = saved current position + emitter.instruction("mov r9d, 1"); // dwMoveMethod = FILE_CURRENT + emitter.instruction("call SetFilePointerEx"); // capture the PHP-visible stream position + emitter.instruction("test eax, eax"); // position query succeeded? + emitter.instruction("jz .Lftruncate_fail"); // translate the Win32 failure + // -- SetFilePointerEx(handle, length, NULL, FILE_BEGIN) -- + emitter.instruction("mov rcx, QWORD PTR [rsp + 40]"); // hFile = converted HANDLE + emitter.instruction("mov rdx, QWORD PTR [rsp + 32]"); // liDistanceToMove = requested length + emitter.instruction("xor r8, r8"); // lpNewFilePointer = NULL + emitter.instruction("xor r9d, r9d"); // dwMoveMethod = FILE_BEGIN + emitter.instruction("call SetFilePointerEx"); // seek to the target offset from the start of the file + emitter.instruction("test eax, eax"); // seek succeeded? + emitter.instruction("jz .Lftruncate_fail"); // translate the Win32 failure + // -- SetEndOfFile(handle) -- + emitter.instruction("mov rcx, QWORD PTR [rsp + 40]"); // hFile = converted HANDLE + emitter.instruction("call SetEndOfFile"); // truncate the file at the current pointer + emitter.instruction("test eax, eax"); // truncate succeeded? + emitter.instruction("jz .Lftruncate_fail"); // translate the Win32 failure + // -- restore the original stream position after resizing -- + emitter.instruction("mov rcx, QWORD PTR [rsp + 40]"); // hFile = converted HANDLE + emitter.instruction("mov rdx, QWORD PTR [rsp + 48]"); // liDistanceToMove = saved position + emitter.instruction("xor r8, r8"); // lpNewFilePointer = NULL + emitter.instruction("xor r9d, r9d"); // dwMoveMethod = FILE_BEGIN + emitter.instruction("call SetFilePointerEx"); // restore PHP's stream cursor + emitter.instruction("test eax, eax"); // restore succeeded? + emitter.instruction("jz .Lftruncate_fail"); // report restoration failure like php-src + emitter.instruction("xor eax, eax"); // return 0 (success) + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lftruncate_fail"); + emitter.instruction("call GetLastError"); // capture the seek/truncate failure + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // retain native Win32 state + emitter.instruction("call __rt_win32_errno_from_code"); // translate failure to POSIX errno + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish observable errno + emitter.instruction("mov rax, -1"); // return -1 (failure) + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lftruncate_bad_fd"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 9"); // EBADF for an invalid CRT descriptor + emitter.instruction("mov rax, -1"); // return -1 (failure) + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lftruncate_invalid_size"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 22"); // EINVAL for a negative target size + emitter.instruction("mov rax, -1"); // return -1 (failure) + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits a writev shim — iterates over iovec array, calling __rt_sys_write for each. +/// +/// SysV: rdi=fd, rsi=iov, rdx=iovcnt. +/// Each iovec is 16 bytes: [iov_base:8, iov_len:8]. +pub(super) fn emit_shim_writev(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_writev"); + emitter.instruction("sub rsp, 40"); // save fd(8) + iov_ptr(8) + iovcnt(8) + total(8) + emitter.instruction("mov QWORD PTR [rsp], rdi"); // save fd + emitter.instruction("mov QWORD PTR [rsp + 8], rsi"); // save iov pointer + emitter.instruction("mov QWORD PTR [rsp + 16], rdx"); // save iovcnt + emitter.instruction("xor rax, rax"); // total written = 0 + emitter.instruction("mov QWORD PTR [rsp + 24], rax"); // save total + emitter.label(".Lwritev_loop"); + emitter.instruction("mov r10, QWORD PTR [rsp + 16]"); // load iovcnt + emitter.instruction("test r10, r10"); // iovcnt == 0? + emitter.instruction("jz .Lwritev_done"); // done if no more iovecs + emitter.instruction("mov r11, QWORD PTR [rsp + 8]"); // load current iov pointer + emitter.instruction("mov rdi, QWORD PTR [rsp]"); // fd + emitter.instruction("mov rsi, QWORD PTR [r11]"); // iov_base + emitter.instruction("mov rdx, QWORD PTR [r11 + 8]"); // iov_len + emitter.instruction("call __rt_sys_write"); // write(fd, iov_base, iov_len) + emitter.instruction("add QWORD PTR [rsp + 8], 16"); // advance iov pointer to next entry + emitter.instruction("sub QWORD PTR [rsp + 16], 1"); // iovcnt-- + emitter.instruction("test rax, rax"); // write returned error? + emitter.instruction("js .Lwritev_done"); // exit on error + emitter.instruction("add QWORD PTR [rsp + 24], rax"); // total += bytes_written + emitter.instruction("jmp .Lwritev_loop"); // continue loop + emitter.label(".Lwritev_done"); + emitter.instruction("mov rax, QWORD PTR [rsp + 24]"); // return total bytes written + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits a statfs shim — delegates to `GetDiskFreeSpaceExW` and fills the three +/// fields `__rt_disk_space` reads out of the Linux-layout `struct statfs` buffer: +/// `f_bsize` (offset 8), `f_blocks` (offset 16), `f_bavail` (offset 32) — see +/// `Platform::statfs_bsize_offset`/`statfs_blocks_offset`/`statfs_bavail_offset` +/// in `platform/target.rs`, which this shim's literal offsets must track. +/// +/// SysV: rdi=path, rsi=statfs buffer base (both MSx64 non-volatile, survive the +/// call). `f_bsize` is reported as 1 so `f_blocks`/`f_bavail` can hold raw byte +/// counts directly (`__rt_disk_space` computes `bytes = f_bsize * block_count`). +/// Returns 0 on success, -1 on failure (leaving the caller's buffer untouched). +pub(super) fn emit_shim_statfs(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_statfs"); + emitter.instruction("sub rsp, 88"); // shadow, two out-qwords, owned path, and native error + emitter.instruction("mov QWORD PTR [rsp + 64], rsi"); // preserve statfs destination across runtime cleanup helpers + emitter.instruction("call __rt_win_utf8_to_utf16"); // strictly convert the filesystem path + emitter.instruction("test rax, rax"); // conversion succeeded? + emitter.instruction("jz .Lstatfs_conversion_fail"); // invalid UTF-8 path + emitter.instruction("mov QWORD PTR [rsp + 48], rax"); // retain owned wide path + emitter.instruction("mov rcx, rax"); // lpDirectoryName = UTF-16 path + emitter.instruction("lea rdx, [rsp + 32]"); // &FreeBytesAvailableToCaller + emitter.instruction("lea r8, [rsp + 40]"); // &TotalNumberOfBytes + emitter.instruction("xor r9, r9"); // lpTotalNumberOfFreeBytes = NULL (unused) + emitter.instruction("call GetDiskFreeSpaceExW"); // query available/total bytes for the Unicode volume path + emitter.instruction("test eax, eax"); // zero return means the query failed + emitter.instruction("jz .Lstatfs_fail"); // return -1 when the path cannot be queried + emitter.instruction("mov rax, QWORD PTR [rsp + 48]"); // owned wide path + emitter.instruction("call __rt_heap_free"); // release converted path after the native query + emitter.instruction("mov rsi, QWORD PTR [rsp + 64]"); // restore statfs destination after cleanup + emitter.instruction("mov DWORD PTR [rsi + 8], 1"); // f_bsize = 1 (so f_blocks/f_bavail already hold raw bytes) + emitter.instruction("mov rax, QWORD PTR [rsp + 40]"); // TotalNumberOfBytes + emitter.instruction("mov QWORD PTR [rsi + 16], rax"); // f_blocks = total bytes (f_bsize == 1) + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // FreeBytesAvailableToCaller + emitter.instruction("mov QWORD PTR [rsi + 32], rax"); // f_bavail = available bytes (f_bsize == 1) + emitter.instruction("xor rax, rax"); // return 0 (success) + emitter.instruction("add rsp, 88"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lstatfs_fail"); + emitter.instruction("call GetLastError"); // capture native query failure before cleanup + emitter.instruction("mov DWORD PTR [rsp + 56], eax"); // preserve native error + emitter.instruction("mov rax, QWORD PTR [rsp + 48]"); // owned wide path + emitter.instruction("call __rt_heap_free"); // release converted path on failure + emitter.instruction("mov eax, DWORD PTR [rsp + 56]"); // restore native error + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // retain native Win32 state + emitter.instruction("call __rt_win32_errno_from_code"); // translate to POSIX errno + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish observable errno + emitter.instruction("mov rax, -1"); // return -1 on failure + emitter.instruction("add rsp, 88"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lstatfs_conversion_fail"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 84"); // EILSEQ: invalid UTF-8 path + emitter.instruction("mov rax, -1"); // return -1 on failure + emitter.instruction("add rsp, 88"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits the `__rt_sys_get_temp_dir` shim: retrieves the Windows temp directory +/// via `GetTempPathW` and returns it as an owned UTF-8 elephc string (heap-allocated, +/// tagged as a persisted string in the uniform heap header, matching the +/// `tempnam.rs` stamping convention). +/// +/// No SysV input arguments. Returns rax = string pointer, rdx = string length +/// (matches `abi::string_result_regs` for x86_64, so the codegen caller needs +/// no register shuffle after `call __rt_sys_get_temp_dir`). Returns an empty +/// string (rax=0, rdx=0) if the native query, allocation, or strict conversion fails. +pub(super) fn emit_shim_sys_get_temp_dir(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_get_temp_dir"); + emitter.instruction("sub rsp, 72"); // shadow space and aligned wide/UTF-8 ownership locals + emitter.instruction("xor ecx, ecx"); // query required WCHAR capacity + emitter.instruction("xor edx, edx"); // no output buffer for the size query + emitter.instruction("call GetTempPathW"); // required WCHAR count including NUL + emitter.instruction("test eax, eax"); // size query succeeded? + emitter.instruction("jz .Lsys_get_temp_dir_query_fail"); // capture native failure + emitter.instruction("mov DWORD PTR [rsp + 40], eax"); // preserve WCHAR capacity + emitter.instruction("movsxd rax, eax"); // widen capacity for allocation + emitter.instruction("shl rax, 1"); // WCHAR uses two bytes + emitter.instruction("call __rt_heap_alloc"); // allocate dynamic UTF-16 buffer + emitter.instruction("test rax, rax"); // wide allocation succeeded? + emitter.instruction("jz .Lsys_get_temp_dir_alloc_fail"); // ENOMEM + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // retain owned UTF-16 buffer + emitter.instruction("mov ecx, DWORD PTR [rsp + 40]"); // WCHAR capacity + emitter.instruction("mov rdx, rax"); // UTF-16 destination + emitter.instruction("call GetTempPathW"); // retrieve temp directory as UTF-16 + emitter.instruction("test eax, eax"); // native retrieval succeeded? + emitter.instruction("jz .Lsys_get_temp_dir_wide_fail"); // capture native failure + emitter.instruction("cmp eax, DWORD PTR [rsp + 40]"); // did the path grow after the size query? + emitter.instruction("jae .Lsys_get_temp_dir_range_fail"); // reject truncated/raced result + emitter.instruction("mov rdi, QWORD PTR [rsp + 32]"); // UTF-16 source + emitter.instruction("xor esi, esi"); // query UTF-8 size without a destination + emitter.instruction("xor edx, edx"); // destination byte capacity = zero + emitter.instruction("call __rt_win_utf16_to_utf8"); // required UTF-8 bytes including NUL + emitter.instruction("test eax, eax"); // strict size query succeeded? + emitter.instruction("jz .Lsys_get_temp_dir_wide_fail"); // capture conversion failure + emitter.instruction("mov DWORD PTR [rsp + 48], eax"); // preserve UTF-8 capacity + emitter.instruction("movsxd rax, eax"); // allocation size includes NUL + emitter.instruction("call __rt_heap_alloc"); // allocate owned UTF-8 string + emitter.instruction("test rax, rax"); // UTF-8 allocation succeeded? + emitter.instruction("jz .Lsys_get_temp_dir_utf8_alloc_fail"); // cleanup wide buffer and return ENOMEM + emitter.instruction(&format!( // owned-string heap kind word + "mov r10, 0x{:x}", + sentinels::x86_64_heap_kind_word(1) + )); + emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the buffer as a persisted elephc string + emitter.instruction("mov QWORD PTR [rsp + 56], rax"); // retain owned UTF-8 result + emitter.instruction("mov rdi, QWORD PTR [rsp + 32]"); // UTF-16 source + emitter.instruction("mov rsi, rax"); // owned UTF-8 destination + emitter.instruction("mov edx, DWORD PTR [rsp + 48]"); // destination byte capacity + emitter.instruction("call __rt_win_utf16_to_utf8"); // perform strict conversion including NUL + emitter.instruction("test eax, eax"); // final conversion succeeded? + emitter.instruction("jz .Lsys_get_temp_dir_conversion_fail"); // release both allocations on failure + emitter.instruction("dec eax"); // exclude terminating NUL from elephc length + emitter.instruction("mov r10, QWORD PTR [rsp + 56]"); // UTF-8 result base + emitter.instruction("test eax, eax"); // path has at least one byte? + emitter.instruction("jz .Lsys_get_temp_dir_trim_done"); // nothing to trim + emitter.instruction("cmp BYTE PTR [r10 + rax - 1], 92"); // trailing Windows backslash? + emitter.instruction("jne .Lsys_get_temp_dir_trim_done"); // preserve non-separator final byte + emitter.instruction("dec eax"); // strip the trailing backslash + emitter.label(".Lsys_get_temp_dir_trim_done"); + emitter.instruction("mov BYTE PTR [r10 + rax], 0"); // terminate after optional trim + emitter.instruction("mov DWORD PTR [rsp + 48], eax"); // preserve result length across wide cleanup + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // owned UTF-16 buffer + emitter.instruction("call __rt_heap_free"); // release temporary wide storage + emitter.instruction("mov rax, QWORD PTR [rsp + 56]"); // return owned UTF-8 pointer + emitter.instruction("mov edx, DWORD PTR [rsp + 48]"); // return UTF-8 byte length + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return owned pointer/length + emitter.label(".Lsys_get_temp_dir_conversion_fail"); + emitter.instruction("call GetLastError"); // capture conversion failure before cleanup + emitter.instruction("mov DWORD PTR [rsp + 64], eax"); // preserve native error code + emitter.instruction("mov rax, QWORD PTR [rsp + 56]"); // owned UTF-8 allocation + emitter.instruction("call __rt_heap_free"); // release failed result + emitter.instruction("jmp .Lsys_get_temp_dir_wide_fail_saved"); // cleanup wide allocation and publish error + emitter.label(".Lsys_get_temp_dir_wide_fail"); + emitter.instruction("call GetLastError"); // capture native/conversion failure + emitter.instruction("mov DWORD PTR [rsp + 64], eax"); // preserve native error code + emitter.label(".Lsys_get_temp_dir_wide_fail_saved"); + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // owned UTF-16 buffer + emitter.instruction("call __rt_heap_free"); // release temporary wide storage + emitter.instruction("mov eax, DWORD PTR [rsp + 64]"); // restore native error code + emitter.instruction("jmp .Lsys_get_temp_dir_publish_native_error"); // map error and return empty string + emitter.label(".Lsys_get_temp_dir_query_fail"); + emitter.instruction("call GetLastError"); // capture initial query failure + emitter.label(".Lsys_get_temp_dir_publish_native_error"); + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // retain native Win32 state + emitter.instruction("call __rt_win32_errno_from_code"); // translate to POSIX errno + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish observable errno + emitter.instruction("jmp .Lsys_get_temp_dir_fail"); // return empty string + emitter.label(".Lsys_get_temp_dir_range_fail"); + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // owned UTF-16 buffer + emitter.instruction("call __rt_heap_free"); // release buffer after a size race + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 34"); // ERANGE + emitter.instruction("jmp .Lsys_get_temp_dir_fail"); // return empty string + emitter.label(".Lsys_get_temp_dir_utf8_alloc_fail"); + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // owned UTF-16 buffer + emitter.instruction("call __rt_heap_free"); // release wide storage after allocation failure + emitter.label(".Lsys_get_temp_dir_alloc_fail"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 12"); // ENOMEM + emitter.label(".Lsys_get_temp_dir_fail"); + emitter.instruction("xor rax, rax"); // empty string pointer + emitter.instruction("xor rdx, rdx"); // empty string length + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits a getdents shim — Windows doesn't have getdents, return -1 (ENOSYS). +/// PHP uses FindFirstFileExW/FindNextFileW for directory iteration on Windows. +pub(super) fn emit_shim_getdents(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_getdents"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 38"); // ENOSYS: callers must use the FindFirstFileExW DIR layer + emitter.instruction("mov rax, -1"); // return -1 (not supported on Windows) + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits a creat shim — maps to __rt_sys_open with O_WRONLY|O_CREAT|O_TRUNC. +pub(super) fn emit_shim_creat(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_creat"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("mov rsi, 0x240"); // O_WRONLY | O_CREAT | O_TRUNC + emitter.instruction("call __rt_sys_open"); // delegate to open shim + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits a newfstatat shim — delegates to msvcrt stat on Windows. +pub(super) fn emit_shim_newfstatat(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_newfstatat"); + emitter.instruction("sub rsp, 8"); // align stack + emitter.instruction("mov rdi, rsi"); // path (skip dirfd arg1) + emitter.instruction("mov rsi, rdx"); // stat buffer + emitter.instruction("call __rt_sys_stat"); // delegate to stat shim + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits the `__rt_sys_opendir` shim: opens a directory stream via +/// `FindFirstFileExW` over `\*`, the real Windows port of POSIX +/// `opendir()` (the same primitive php-src's own win32 dirent layer uses). +/// +/// Allocates a persistent DIR state block through `__rt_heap_alloc` (624 +/// bytes total), shared by `__rt_sys_readdir`/`__rt_sys_closedir`/ +/// `__rt_sys_rewinddir` below: +/// +0 HANDLE hFind +/// +8 i64 first_pending (1 = the WIN32_FIND_DATAW at +24 is +/// FindFirstFileExW's still-unconsumed first entry; 0 = the next +/// `readdir()` call must fetch a fresh entry via `FindNextFileW`) +/// +16 WCHAR* pattern — the heap-allocated "\*" search pattern, +/// kept alive (not freed until `closedir`) so `rewinddir` can +/// `FindClose` + reopen it +/// +24 WIN32_FIND_DATAW (592 bytes; `cFileName` at +24+44 = +68) +/// +616 a Linux-`dirent`-layout scratch buffer that `__rt_sys_readdir` +/// fills per call — `d_name` lands at `Platform::dirent_name_offset()` +/// (19 on Windows, mirroring the Linux layout `opendir.rs`/ +/// `scandir.rs`/`readdir.rs` already read on every other target) +/// +/// Returns the DIR state pointer in rax, or NULL (rax=0) on failure — the +/// exact sentinel `opendir.rs`'s `test rax,rax; jz` already handles. +/// SysV: rdi = a NUL-terminated path C string (already produced by +/// `__rt_cstr` at the call site). +pub(super) fn emit_shim_opendir(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_opendir"); + emitter.instruction("sub rsp, 104"); // shadow(32), FindFirstFileExW stack args, and aligned locals + emitter.instruction("mov QWORD PTR [rsp + 32], rdi"); // save the path C-string pointer + // -- measure the path length -- + emitter.instruction("xor rax, rax"); // scan index + emitter.label(".Lopendir_strlen"); + emitter.instruction("cmp BYTE PTR [rdi + rax], 0"); // terminating NUL? + emitter.instruction("je .Lopendir_strlen_done"); // path length is now known + emitter.instruction("inc rax"); // count one more path byte + emitter.instruction("jmp .Lopendir_strlen"); // continue scanning the path + emitter.label(".Lopendir_strlen_done"); + emitter.instruction("mov QWORD PTR [rsp + 40], rax"); // save the path length + // -- build the "\*" search pattern -- + emitter.instruction("add rax, 3"); // + '\' + '*' + NUL + emitter.instruction("call __rt_heap_alloc"); // rax = the search-pattern buffer + emitter.instruction("mov QWORD PTR [rsp + 48], rax"); // save the pattern buffer pointer + emitter.instruction("mov r10, rax"); // dest cursor + emitter.instruction("mov r11, QWORD PTR [rsp + 32]"); // src = the original path + emitter.instruction("xor r9, r9"); // copy index + emitter.instruction("mov r8, QWORD PTR [rsp + 40]"); // path length + emitter.label(".Lopendir_copy_path"); + emitter.instruction("cmp r9, r8"); // copied the whole path? + emitter.instruction("jae .Lopendir_copy_path_done"); // → append the "\*" suffix + emitter.instruction("movzx eax, BYTE PTR [r11 + r9]"); // load the next path byte + emitter.instruction("mov BYTE PTR [r10 + r9], al"); // store it into the pattern buffer + emitter.instruction("inc r9"); // advance the copy index + emitter.instruction("jmp .Lopendir_copy_path"); // continue copying the path + emitter.label(".Lopendir_copy_path_done"); + emitter.instruction("mov BYTE PTR [r10 + r8], 92"); // append '\' + emitter.instruction("lea rax, [r8 + 1]"); // index of the wildcard byte + emitter.instruction("mov BYTE PTR [r10 + rax], 42"); // append '*' + emitter.instruction("lea rax, [r8 + 2]"); // index of the terminator + emitter.instruction("mov BYTE PTR [r10 + rax], 0"); // NUL-terminate the search pattern + // -- convert the UTF-8 pattern once and retain the UTF-16 form for rewinddir -- + emitter.instruction("mov rdi, QWORD PTR [rsp + 48]"); // UTF-8 search pattern + emitter.instruction("call __rt_win_utf8_to_utf16"); // allocate a strict UTF-16 pattern + emitter.instruction("mov QWORD PTR [rsp + 64], rax"); // preserve the converted pattern + emitter.instruction("mov rax, QWORD PTR [rsp + 48]"); // temporary UTF-8 pattern + emitter.instruction("call __rt_heap_free"); // only the wide pattern remains owned by DIR + emitter.instruction("mov rax, QWORD PTR [rsp + 64]"); // converted search pattern + emitter.instruction("test rax, rax"); // conversion succeeded? + emitter.instruction("jz .Lopendir_conversion_fail"); // reject invalid UTF-8 paths cleanly + emitter.instruction("mov QWORD PTR [rsp + 48], rax"); // retain UTF-16 pattern in the normal cleanup slot + // -- allocate the persistent DIR state block -- + emitter.instruction("mov rax, 1680"); // header + WIN32_FIND_DATAW + UTF-8 dirent scratch + emitter.instruction("call __rt_heap_alloc"); // rax = the DIR state block + emitter.instruction("mov QWORD PTR [rsp + 56], rax"); // save the DIR state pointer + emitter.instruction("test rax, rax"); // state allocation succeeded? + emitter.instruction("jz .Lopendir_fail"); // release the converted pattern on allocation failure + // -- FindFirstFileExW(pattern, FindExInfoBasic, &data, FindExSearchNameMatch, NULL, LARGE_FETCH) -- + emitter.instruction("mov rcx, QWORD PTR [rsp + 48]"); // lpFileName = the search pattern + emitter.instruction("mov edx, 1"); // fInfoLevelId = FindExInfoBasic + emitter.instruction("mov r8, QWORD PTR [rsp + 56]"); // DIR state + emitter.instruction("add r8, 24"); // &DIRstate.data (WIN32_FIND_DATAW) + emitter.instruction("xor r9d, r9d"); // fSearchOp = FindExSearchNameMatch + emitter.instruction("mov QWORD PTR [rsp + 32], 0"); // lpSearchFilter = NULL + emitter.instruction("mov QWORD PTR [rsp + 40], 2"); // FIND_FIRST_EX_LARGE_FETCH + emitter.instruction("call FindFirstFileExW"); // open Unicode enumeration over the first entry + emitter.instruction("cmp rax, -1"); // INVALID_HANDLE_VALUE? + emitter.instruction("je .Lopendir_fail"); // opendir failed + emitter.instruction("mov r10, QWORD PTR [rsp + 56]"); // DIR state + emitter.instruction("mov QWORD PTR [r10], rax"); // DIRstate.hFind = the search handle + emitter.instruction("mov QWORD PTR [r10 + 8], 1"); // DIRstate.first_pending = 1 + emitter.instruction("mov rax, QWORD PTR [rsp + 48]"); // the search pattern buffer + emitter.instruction("mov QWORD PTR [r10 + 16], rax"); // DIRstate.pattern = the pattern (kept alive for rewinddir) + emitter.instruction("mov rax, r10"); // return the DIR state pointer + emitter.instruction("add rsp, 104"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lopendir_fail"); + emitter.instruction("mov rax, QWORD PTR [rsp + 48]"); // the unused search pattern buffer + emitter.instruction("call __rt_heap_free"); // release it + emitter.instruction("mov rax, QWORD PTR [rsp + 56]"); // the unused DIR state block + emitter.instruction("test rax, rax"); // was the state block allocated? + emitter.instruction("jz .Lopendir_fail_done"); // skip freeing a NULL allocation + emitter.instruction("call __rt_heap_free"); // release it + emitter.label(".Lopendir_fail_done"); + emitter.instruction("xor eax, eax"); // sentinel: NULL DIR* (opendir failed) + emitter.instruction("add rsp, 104"); // restore stack + emitter.instruction("ret"); // return the failure sentinel + emitter.label(".Lopendir_conversion_fail"); + emitter.instruction("xor eax, eax"); // invalid UTF-8 cannot name a Windows path + emitter.instruction("add rsp, 104"); // restore stack + emitter.instruction("ret"); // return NULL + emitter.blank(); +} + +/// Emits the `__rt_sys_readdir` shim: advances the DIR state opened by +/// `__rt_sys_opendir` above (`FindNextFileW`, or the `FindFirstFileExW` entry +/// already pending from `opendir`) and copies the matched entry name into +/// the Linux-`dirent`-layout scratch buffer embedded in the DIR state — +/// `d_name` lands at `Platform::dirent_name_offset()`, exactly what every +/// shared consumer (`opendir.rs`/`scandir.rs`/`readdir.rs`) already reads on +/// every other target. php-src semantics: NO sorting (raw OS enumeration +/// order) and `.`/`..` ARE returned — contrast with the `glob` shim in +/// `shims_c_symbols.rs`, which always filters them. +/// +/// Returns a pointer to the scratch dirent, or NULL (rax=0) at +/// end-of-directory — the sentinel every consumer already treats as "no +/// more entries". SysV: rdi = DIR state pointer. +pub(super) fn emit_shim_readdir(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_readdir"); + emitter.instruction("sub rsp, 40"); // shadow(32) + pad(8), 16B aligned + emitter.instruction("cmp QWORD PTR [rdi + 8], 0"); // DIRstate.first_pending? + emitter.instruction("je .Lreaddir_next"); // 0: fetch a fresh entry via FindNextFileW + emitter.instruction("mov QWORD PTR [rdi + 8], 0"); // consume the pending FindFirstFileExW entry + emitter.instruction("jmp .Lreaddir_build"); // the data at DIRstate+24 already holds this entry + emitter.label(".Lreaddir_next"); + emitter.instruction("mov rcx, QWORD PTR [rdi]"); // DIRstate.hFind + emitter.instruction("lea rdx, [rdi + 24]"); // &DIRstate.data + emitter.instruction("call FindNextFileW"); // advance to the next Unicode directory entry + emitter.instruction("test eax, eax"); // more entries? + emitter.instruction("jnz .Lreaddir_build"); // yes: convert the returned entry + emitter.instruction("call GetLastError"); // distinguish EOF from a native enumeration failure + emitter.instruction("cmp eax, 18"); // ERROR_NO_MORE_FILES? + emitter.instruction("je .Lreaddir_end"); // clean end of directory leaves errno unchanged + emitter.instruction("jmp .Lreaddir_native_error"); // publish the enumeration failure + emitter.label(".Lreaddir_build"); + // -- zero the Linux dirent header fields; only d_name (below) is read by any consumer -- + emitter.instruction("lea rax, [rdi + 616]"); // scratch dirent after WIN32_FIND_DATAW + emitter.instruction("mov QWORD PTR [rax], 0"); // d_ino = 0 + emitter.instruction("mov QWORD PTR [rax + 8], 0"); // d_off = 0 + emitter.instruction("mov DWORD PTR [rax + 16], 0"); // d_reclen = 0, d_type = 0 + // -- convert cFileName from UTF-16 into PHP's UTF-8 dirent name -- + emitter.instruction("mov QWORD PTR [rsp + 32], rdi"); // preserve DIR state across the helper call + emitter.instruction("lea rsi, [rax + 19]"); // destination = dirent.d_name + emitter.instruction("mov rdx, 1040"); // worst-case UTF-8 capacity for MAX_PATH WCHARs + emitter.instruction("lea rdi, [rdi + 68]"); // source = WIN32_FIND_DATAW.cFileName + emitter.instruction("call __rt_win_utf16_to_utf8"); // convert the native entry name + emitter.instruction("test eax, eax"); // conversion succeeded? + emitter.instruction("jnz .Lreaddir_converted"); // return the converted entry + emitter.instruction("call GetLastError"); // capture strict UTF conversion failure + emitter.instruction("jmp .Lreaddir_native_error"); // publish rather than pretending it was EOF + emitter.label(".Lreaddir_converted"); + emitter.instruction("mov rdi, QWORD PTR [rsp + 32]"); // restore DIR state + emitter.instruction("lea rax, [rdi + 616]"); // return the scratch dirent pointer + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return the scratch dirent pointer (still in rax) + emitter.label(".Lreaddir_native_error"); + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // retain native Win32 state + emitter.instruction("call __rt_win32_errno_from_code"); // translate failure to POSIX errno + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish observable errno + emitter.label(".Lreaddir_end"); + emitter.instruction("xor eax, eax"); // sentinel: NULL dirent (end-of-directory) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return the failure sentinel + emitter.blank(); +} + +/// Emits the `__rt_sys_closedir` shim: closes the Win32 search handle +/// (`FindClose`) and releases the DIR state block `__rt_sys_opendir` +/// allocated, including the kept-alive search pattern string. Returns zero on +/// success, or publishes the translated `FindClose` error and returns -1. +/// SysV: rdi = DIR state pointer. +pub(super) fn emit_shim_closedir(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_closedir"); + emitter.instruction("sub rsp, 56"); // shadow plus saved state and close result, 16B aligned + emitter.instruction("mov QWORD PTR [rsp + 32], rdi"); // save the DIR state pointer + emitter.instruction("mov rcx, QWORD PTR [rdi]"); // DIRstate.hFind + emitter.instruction("call FindClose"); // release the Win32 search handle + emitter.instruction("test eax, eax"); // close succeeded? + emitter.instruction("jnz .Lclosedir_close_ok"); // preserve zero success marker + emitter.instruction("call GetLastError"); // capture close failure before heap cleanup + emitter.instruction("mov DWORD PTR [rsp + 40], eax"); // retain native error across cleanup + emitter.instruction("jmp .Lclosedir_cleanup"); // release owned allocations on every path + emitter.label(".Lclosedir_close_ok"); + emitter.instruction("mov DWORD PTR [rsp + 40], 0"); // success marker + emitter.label(".Lclosedir_cleanup"); + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // DIR state pointer + emitter.instruction("mov rax, QWORD PTR [rax + 16]"); // DIRstate.pattern + emitter.instruction("call __rt_heap_free"); // release the kept-alive search pattern + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // DIR state pointer + emitter.instruction("call __rt_heap_free"); // release the DIR state block itself + emitter.instruction("mov eax, DWORD PTR [rsp + 40]"); // restore close result/error + emitter.instruction("test eax, eax"); // was FindClose successful? + emitter.instruction("jnz .Lclosedir_error"); // publish close failure after cleanup + emitter.instruction("xor eax, eax"); // closedir success + emitter.instruction("add rsp, 56"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lclosedir_error"); + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // retain native Win32 state + emitter.instruction("call __rt_win32_errno_from_code"); // translate failure to POSIX errno + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish observable errno + emitter.instruction("mov rax, -1"); // POSIX closedir failure + emitter.instruction("add rsp, 56"); // restore stack + emitter.instruction("ret"); // return failure + emitter.blank(); +} + +/// Emits the `__rt_sys_rewinddir` shim: rewinds a directory stream back to +/// its first entry via `FindClose` + `FindFirstFileExW` against the DIR +/// state's kept-alive search pattern — Windows has no native "seek to +/// start" primitive for a search handle, so php-src's own win32 rewinddir +/// also closes and reopens the search. SysV: rdi = DIR state pointer. Void +/// return, matching the shared `rewinddir.rs` consumer, which discards the +/// result. +pub(super) fn emit_shim_rewinddir(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_rewinddir"); + emitter.instruction("sub rsp, 56"); // shadow space, two stack args, and aligned state spill + emitter.instruction("mov QWORD PTR [rsp + 48], rdi"); // save the DIR state pointer + emitter.instruction("mov rcx, QWORD PTR [rdi]"); // DIRstate.hFind + emitter.instruction("call FindClose"); // close the current search handle + emitter.instruction("mov rdi, QWORD PTR [rsp + 48]"); // reload the DIR state pointer + emitter.instruction("mov rcx, QWORD PTR [rdi + 16]"); // DIRstate.pattern (the kept-alive search pattern) + emitter.instruction("mov edx, 1"); // fInfoLevelId = FindExInfoBasic + emitter.instruction("lea r8, [rdi + 24]"); // &DIRstate.data + emitter.instruction("xor r9d, r9d"); // fSearchOp = FindExSearchNameMatch + emitter.instruction("mov QWORD PTR [rsp + 32], 0"); // lpSearchFilter = NULL + emitter.instruction("mov QWORD PTR [rsp + 40], 2"); // FIND_FIRST_EX_LARGE_FETCH + emitter.instruction("call FindFirstFileExW"); // reopen Unicode enumeration at the first entry + emitter.instruction("mov rdi, QWORD PTR [rsp + 48]"); // reload the DIR state pointer + emitter.instruction("mov QWORD PTR [rdi], rax"); // DIRstate.hFind = the reopened handle (or INVALID_HANDLE_VALUE) + emitter.instruction("mov QWORD PTR [rdi + 8], 0"); // clear first_pending by default + emitter.instruction("cmp rax, -1"); // did the reopen fail? + emitter.instruction("je .Lrewinddir_done"); // failure: leave first_pending cleared — readdir sees the invalid handle fail cleanly + emitter.instruction("mov QWORD PTR [rdi + 8], 1"); // success: the just-fetched entry is unconsumed + emitter.label(".Lrewinddir_done"); + emitter.instruction("add rsp, 56"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits the `__rt_sys_mkstemp` shim: creates a unique temp file by +/// rewriting the mutable template's trailing "XXXXXX" placeholder in place +/// — matching POSIX `mkstemp()`'s exact contract of mutating the SAME +/// buffer at the SAME length, which `tempnam.rs`/`streams_ext.rs` both rely +/// on (they compute the returned string's length arithmetically from the +/// original directory/prefix lengths rather than re-scanning for NUL, so +/// the rewritten suffix must stay exactly 6 bytes) — with pseudo-random +/// alphanumeric characters, then exclusively creating that path via +/// `CreateFileW(..., CREATE_NEW, ...)`. `CREATE_NEW` fails with +/// `ERROR_FILE_EXISTS`/`ERROR_ALREADY_EXISTS` on a collision, giving the +/// same atomic-uniqueness guarantee real `mkstemp()` provides; a collision +/// retries with a freshly reseeded suffix (bounded at 8 attempts). +/// +/// On success, explicitly clears the read-only attribute some Windows +/// configurations default new files to (php-src's own temp-file creation +/// does the equivalent `chmod(path, 0600)` for the same reason) and returns +/// the new file as a Microsoft CRT descriptor via `_open_osfhandle`, matching +/// `__rt_sys_open` and making the result valid for `_dup`/`_dup2` as well as +/// direct Win32 I/O after `__rt_fd_to_handle` conversion. +/// +/// SysV: rdi = the mutable template buffer (NUL-terminated, ending in +/// "XXXXXX"). Returns the open fd in eax, or -1 on failure. +pub(super) fn emit_shim_mkstemp(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_mkstemp"); + emitter.instruction("sub rsp, 104"); // shadow(32)+CreateFileW stack args(24)+locals(48), 16B aligned + emitter.instruction("mov QWORD PTR [rsp + 56], rdi"); // save the template buffer pointer + // -- measure the template length; the last 6 bytes before the NUL are always "XXXXXX" -- + emitter.instruction("xor rax, rax"); // scan index + emitter.label(".Lmkstemp_strlen"); + emitter.instruction("cmp BYTE PTR [rdi + rax], 0"); // terminating NUL? + emitter.instruction("je .Lmkstemp_strlen_done"); // template length is now known + emitter.instruction("inc rax"); // count one more template byte + emitter.instruction("jmp .Lmkstemp_strlen"); // continue scanning the template + emitter.label(".Lmkstemp_strlen_done"); + emitter.instruction("lea rax, [rdi + rax - 6]"); // xpos = the start of the "XXXXXX" placeholder + emitter.instruction("mov QWORD PTR [rsp + 64], rax"); // save xpos + emitter.instruction("mov QWORD PTR [rsp + 88], 0"); // attempt = 0 + emitter.label(".Lmkstemp_retry"); + // -- reseed from QueryPerformanceCounter, mixed with the buffer address and the attempt index -- + emitter.instruction("lea rcx, [rsp + 72]"); // &LARGE_INTEGER out-param + emitter.instruction("call QueryPerformanceCounter"); // high-resolution counter (changes every retry) + emitter.instruction("mov rax, QWORD PTR [rsp + 72]"); // the 64-bit counter value + emitter.instruction("mov rcx, QWORD PTR [rsp + 56]"); // the template buffer address + emitter.instruction("xor rax, rcx"); // mix in the buffer address + emitter.instruction("mov rcx, QWORD PTR [rsp + 88]"); // the attempt index + emitter.instruction("imul rcx, rcx, 40503"); // spread the attempt index across the seed (Knuth multiplicative constant) + emitter.instruction("xor rax, rcx"); // mix in the attempt index + emitter.instruction("mov QWORD PTR [rsp + 80], rax"); // save the seed + for i in 0..6u32 { + emitter.instruction("mov rax, QWORD PTR [rsp + 80]"); // reload the seed + emitter.instruction("xor rdx, rdx"); // clear the high dividend half + emitter.instruction("mov rcx, 36"); // 36 alphanumeric characters + emitter.instruction("div rcx"); // rdx = seed % 36 + emitter.instruction("cmp rdx, 10"); // digit or letter? + emitter.instruction(&format!("jae .Lmkstemp_alpha_{}", i)); // → letter + emitter.instruction("add rdx, 48"); // '0' + n + emitter.instruction(&format!("jmp .Lmkstemp_store_{}", i)); // → store the character + emitter.label(&format!(".Lmkstemp_alpha_{}", i)); + emitter.instruction("add rdx, 87"); // 'a' + (n - 10) + emitter.label(&format!(".Lmkstemp_store_{}", i)); + emitter.instruction("mov r8, QWORD PTR [rsp + 64]"); // xpos + emitter.instruction(&format!("mov BYTE PTR [r8 + {}], dl", i)); // write the generated character + emitter.instruction("mov rax, QWORD PTR [rsp + 80]"); // reload the seed + emitter.instruction("mov rcx, 1103515245"); // classic LCG multiplier + emitter.instruction("mul rcx"); // rax = seed * multiplier + emitter.instruction("add rax, 12345"); // classic LCG increment + emitter.instruction("mov QWORD PTR [rsp + 80], rax"); // save the advanced seed + } + // -- convert each candidate to UTF-16, then create it atomically with CreateFileW -- + emitter.instruction("mov rdi, QWORD PTR [rsp + 56]"); // UTF-8 template holding the fresh suffix + emitter.instruction("call __rt_win_utf8_to_utf16"); // allocate the native Unicode path + emitter.instruction("mov QWORD PTR [rsp + 72], rax"); // preserve the candidate wide path + emitter.instruction("test rax, rax"); // conversion succeeded? + emitter.instruction("jz .Lmkstemp_hard_fail"); // invalid UTF-8 cannot name a Windows temp file + emitter.instruction("mov rcx, rax"); // lpFileName = converted candidate + emitter.instruction("mov rdx, 0xC0000000"); // GENERIC_READ | GENERIC_WRITE + emitter.instruction("xor r8, r8"); // dwShareMode = 0 (exclusive, matching mkstemp()'s own guarantee) + emitter.instruction("xor r9, r9"); // lpSecurityAttributes = NULL + emitter.instruction("mov QWORD PTR [rsp + 32], 1"); // dwCreationDisposition = CREATE_NEW (fails if the path exists) + emitter.instruction("mov QWORD PTR [rsp + 40], 128"); // dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL + emitter.instruction("mov QWORD PTR [rsp + 48], 0"); // hTemplateFile = NULL + emitter.instruction("call CreateFileW"); // exclusively create the Unicode temp file + emitter.instruction("mov QWORD PTR [rsp + 96], rax"); // preserve HANDLE across cleanup and error lookup + emitter.instruction("cmp rax, -1"); // INVALID_HANDLE_VALUE? + emitter.instruction("jne .Lmkstemp_success"); // created: return this handle as the fd + emitter.instruction("call GetLastError"); // inspect why CreateFileW failed + emitter.instruction("mov DWORD PTR [rsp + 80], eax"); // preserve the raw failure code across heap cleanup + emitter.instruction("mov rax, QWORD PTR [rsp + 72]"); // wide candidate allocation + emitter.instruction("call __rt_heap_free"); // release failed candidate path + emitter.instruction("mov eax, DWORD PTR [rsp + 80]"); // reload the original CreateFileW error + emitter.instruction("cmp eax, 80"); // ERROR_FILE_EXISTS? + emitter.instruction("je .Lmkstemp_next_attempt"); // → retry with a fresh suffix + emitter.instruction("cmp eax, 183"); // ERROR_ALREADY_EXISTS? + emitter.instruction("je .Lmkstemp_next_attempt"); // → retry with a fresh suffix + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // preserve the native hard-failure code + emitter.instruction("call __rt_win32_errno_from_code"); // translate the hard failure to POSIX errno + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish the translated temp-file failure + emitter.instruction("jmp .Lmkstemp_hard_fail"); // any other failure: give up immediately + emitter.label(".Lmkstemp_next_attempt"); + emitter.instruction("mov rax, QWORD PTR [rsp + 88]"); // reload the attempt index + emitter.instruction("inc rax"); // advance to the next attempt + emitter.instruction("mov QWORD PTR [rsp + 88], rax"); // save the updated attempt index + emitter.instruction("cmp rax, 8"); // exhausted the retry budget? + emitter.instruction("jl .Lmkstemp_retry"); // retry with a fresh suffix + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], 183"); // collision budget exhausted with ERROR_ALREADY_EXISTS + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 17"); // publish EEXIST for the exhausted collision budget + emitter.label(".Lmkstemp_hard_fail"); + emitter.instruction("mov rax, -1"); // sentinel: mkstemp failed + emitter.instruction("add rsp, 104"); // restore stack + emitter.instruction("ret"); // return the failure sentinel + emitter.label(".Lmkstemp_success"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 72]"); // final unique Unicode path + emitter.instruction("mov rdx, 128"); // FILE_ATTRIBUTE_NORMAL: guards against a read-only default on some configs + emitter.instruction("call SetFileAttributesW"); // ensure the Unicode temp file is writable + emitter.instruction("mov rax, QWORD PTR [rsp + 72]"); // wide candidate allocation + emitter.instruction("call __rt_heap_free"); // release path after all Win32 path calls + emitter.instruction("mov rcx, QWORD PTR [rsp + 96]"); // raw Win32 handle to adopt + emitter.instruction("mov edx, 0x8002"); // _O_BINARY | _O_RDWR + emitter.instruction("call _open_osfhandle"); // create a CRT descriptor for dup/read/write/close + emitter.instruction("cmp eax, -1"); // descriptor allocation failed? + emitter.instruction("je .Lmkstemp_crt_fail"); // close the unowned raw handle + emitter.instruction("add rsp, 104"); // restore stack + emitter.instruction("ret"); // return the CRT file descriptor + emitter.label(".Lmkstemp_crt_fail"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 96]"); // HANDLE remains caller-owned after conversion failure + emitter.instruction("call CloseHandle"); // avoid leaking the temporary file handle + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 24"); // EMFILE: no CRT descriptor slot available + emitter.instruction("mov rax, -1"); // mkstemp failure + emitter.instruction("add rsp, 104"); // restore stack + emitter.instruction("ret"); // return failure + emitter.blank(); +} diff --git a/src/codegen_support/runtime/win32/shims_misc.rs b/src/codegen_support/runtime/win32/shims_misc.rs new file mode 100644 index 0000000000..1f1b0033ac --- /dev/null +++ b/src/codegen_support/runtime/win32/shims_misc.rs @@ -0,0 +1,905 @@ +//! Win32 shims for the remaining syscall/libc grab-bag: unsupported-syscall +//! fallback, exit, argv init, mmap/munmap/brk, getpid/getrandom, the math/FP +//! family, strtod/strtol/snprintf, ioctl, dup/getuid/kill/uname/sysinfo, +//! execve/futex/mprotect. + +use crate::codegen::emit::Emitter; + +/// Emits the `__rt_unsupported_syscall` diagnostic helper. +/// +/// The Windows syscall->shim transform rewrites any Linux syscall number with +/// no Win32 shim into `mov eax, ` + `call __rt_unsupported_syscall` (instead +/// of a silent `int3`). Entered with the Linux syscall number in `eax`, this +/// helper prints `unsupported syscall: \n` to stderr via a manual base-10 +/// itoa (no libc) and calls `ExitProcess(70)`. It is emitted unconditionally so +/// the transform always has a target, even if unreferenced in a given build. +pub(super) fn emit_shim_unsupported_syscall(emitter: &mut Emitter) { + emitter.label_global("__rt_unsupported_syscall"); + emitter.instruction("sub rsp, 120"); // frame: shadow(32)+overlapped+written+msg+itoa scratch, 16B aligned + emitter.instruction("mov r15d, eax"); // stash syscall number (eax is clobbered by every Win32 call) + // -- copy the "unsupported syscall: " prefix into the message buffer -- + emitter.instruction("cld"); // ensure forward direction for the string copy + emitter.instruction("lea rsi, [rip + __rt_unsup_prefix]"); // source = constant prefix string + emitter.instruction("lea rdi, [rsp + 48]"); // dest = message buffer start + emitter.instruction("mov ecx, 21"); // prefix length (\"unsupported syscall: \") + emitter.instruction("rep movsb"); // copy the 21 prefix bytes; rdi now points past the prefix + // -- manual base-10 itoa of the number into scratch (least-significant first) -- + emitter.instruction("mov eax, r15d"); // working value = syscall number + emitter.instruction("lea rsi, [rsp + 112]"); // rsi = one past the itoa scratch end + emitter.instruction("mov ecx, 10"); // decimal divisor + emitter.label(".Lunsup_itoa"); + emitter.instruction("xor edx, edx"); // clear the high dividend half before dividing + emitter.instruction("div ecx"); // eax /= 10, edx = eax % 10 + emitter.instruction("add dl, 48"); // convert the remainder to an ASCII digit ('0') + emitter.instruction("dec rsi"); // move one byte toward the front of the scratch + emitter.instruction("mov [rsi], dl"); // store the digit + emitter.instruction("test eax, eax"); // any higher-order digits remaining? + emitter.instruction("jnz .Lunsup_itoa"); // loop until the quotient reaches zero + // -- append the digits (now in order at [rsi..scratch_end]) after the prefix -- + emitter.instruction("lea rdx, [rsp + 112]"); // sentinel = itoa scratch end + emitter.label(".Lunsup_copy"); + emitter.instruction("mov al, [rsi]"); // load the next digit + emitter.instruction("mov [rdi], al"); // append it to the message buffer + emitter.instruction("inc rsi"); // advance the source pointer + emitter.instruction("inc rdi"); // advance the destination pointer + emitter.instruction("cmp rsi, rdx"); // reached the end of the digits? + emitter.instruction("jne .Lunsup_copy"); // keep copying digits + emitter.instruction("mov BYTE PTR [rdi], 10"); // append a trailing newline + emitter.instruction("inc rdi"); // include the newline in the length + emitter.instruction("lea rax, [rsp + 48]"); // message buffer start + emitter.instruction("sub rdi, rax"); // rdi = total message length + emitter.instruction("mov r14, rdi"); // stash length in a callee-saved reg (survives the calls) + // -- WriteFile(stderr, &msg, len, &written, NULL) -- + emitter.instruction("mov ecx, -12"); // STD_ERROR_HANDLE + emitter.instruction("call GetStdHandle"); // rax = stderr handle + emitter.instruction("mov rcx, rax"); // handle (arg1) + emitter.instruction("lea rdx, [rsp + 48]"); // &msg (arg2) + emitter.instruction("mov r8, r14"); // len (arg3) + emitter.instruction("lea r9, [rsp + 40]"); // &written (arg4), off the arg5 slot + emitter.instruction("mov QWORD PTR [rsp + 32], 0"); // lpOverlapped = NULL (arg5 slot) + emitter.instruction("call WriteFile"); // write the diagnostic to stderr + // -- ExitProcess(70) (never returns) -- + emitter.instruction("mov ecx, 70"); // process exit code 70 + emitter.instruction("call ExitProcess"); // terminate the process + // -- constant prefix string in .data -- + emitter.raw(".data"); + emitter.raw("__rt_unsup_prefix:"); + emitter.raw(" .ascii \"unsupported syscall: \""); + emitter.raw(".text"); + emitter.blank(); +} + +/// Emits a shim that calls `ExitProcess` with the exit code from rdi. +/// +/// Forces 16-byte stack alignment before the call so both the implicit +/// end-of-main exit (enters the shim at rsp = 0 mod 16, after the epilogue's +/// `pop rbp` leaves it at 8) and explicit `exit($n)` reach `ExitProcess` +/// correctly aligned; Wine's process-exit path uses aligned SSE and faults +/// otherwise. The shim never returns, so clobbering rsp with the `and` is safe. +/// +/// Alignment arithmetic: `and rsp, -16` forces rsp ≡ 0 mod 16 (the shim can be +/// reached at any alignment, so it must force-align). After that, the prologue +/// `sub rsp, N` MUST have `N ≡ 0 mod 16` so rsp stays ≡ 0 at each `call` site — +/// the opposite rule from shims entered normally (rsp ≡ 8 at entry, which need +/// `N ≡ 8 mod 16`). Using `N ≡ 8` here (e.g. 40) would leave rsp ≡ 8 at the +/// `call __rt_winsock_cleanup` and `call ExitProcess` sites, misaligning the +/// SSE registers Wine's process-exit path reads and crashing with a #GP. +pub(super) fn emit_shim_exit(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_exit"); + emitter.instruction("and rsp, -16"); // force rsp ≡ 0 mod 16 before any Win32 call (shim never returns; clobbering rsp is safe) + emitter.instruction("sub rsp, 48"); // shadow(32) + spill(8) + pad(8), 16-byte aligned (and rsp,-16 forces ≡0, so sub must be ≡0 mod 16) + emitter.instruction("mov QWORD PTR [rsp + 32], rdi"); // spill the exit code (rdi is volatile on MSx64) across the cleanup call + // -- release process-bootstrap argument storage before terminating -- + emitter.instruction("call __rt_sys_free_argv"); // release tracked UTF-8 argv strings and their pointer table + // -- release Winsock resources before terminating -- + emitter.instruction("call __rt_winsock_cleanup"); // WSACleanup() — safe to call even if WSAStartup was never invoked + emitter.instruction("mov rcx, QWORD PTR [rsp + 32]"); // MSx64 arg1 = exit code (reloaded after the cleanup call) + emitter.instruction("call ExitProcess"); // terminate the process (never returns) + emitter.blank(); +} + +/// Emits the non-returning emergency exit used when a generated function +/// prologue detects that its next frame would consume an active Fiber guard. +/// +/// This path intentionally skips heap, argv, Winsock, and Fiber cleanup: the +/// current stack is nearly exhausted, so invoking arbitrary cleanup code could +/// consume the remaining emergency reserve and fault before `ExitProcess`. +pub(super) fn emit_fiber_stack_overflow_abort(emitter: &mut Emitter) { + emitter.label_global("__rt_win_fiber_stack_overflow_abort"); + emitter.instruction("and rsp, -16"); // force a known alignment on the still-usable Fiber stack + emitter.instruction("sub rsp, 32"); // reserve only the mandatory MSx64 shadow space + emitter.instruction("mov ecx, 134"); // conventional abort-style nonzero process status + emitter.instruction("call ExitProcess"); // terminate immediately without unsafe low-stack cleanup + emitter.instruction("ud2"); // preserve non-returning semantics if ExitProcess unexpectedly returns + emitter.blank(); +} + +/// Emits the `__rt_sys_init_argv` shim that populates `_global_argc` and +/// `_global_argv` from the Win32 command line on Windows x86_64. +/// +/// The shim ignores the width-ambiguous CRT entry registers and asks shell32 to +/// parse `GetCommandLineW` with the native Windows quote/backslash rules. Each +/// returned UTF-16 argument is strictly converted into its own persistent UTF-8 +/// allocation; the shell32 array is released with `LocalFree`, while partial +/// failures unwind every argument and the pointer table before publishing a safe +/// empty bootstrap state. +pub(super) fn emit_shim_sys_init_argv(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_init_argv"); + emitter.instruction("sub rsp, 72"); // shadow space and argc/argv/loop ownership locals + emitter.instruction("call GetCommandLineW"); // retrieve immutable UTF-16 process command line + emitter.instruction("mov rcx, rax"); // lpCmdLine + emitter.instruction("lea rdx, [rsp + 32]"); // pNumArgs + emitter.instruction("call CommandLineToArgvW"); // apply native Windows quoting/backslash rules + emitter.instruction("test rax, rax"); // parsing succeeded? + emitter.instruction("jz .Linit_argv_w_parse_fail"); // capture shell32 failure + emitter.instruction("mov QWORD PTR [rsp + 40], rax"); // LocalFree-owned LPWSTR array + emitter.instruction("mov eax, DWORD PTR [rsp + 32]"); // clean zero-extended argc + emitter.instruction("mov DWORD PTR [rsp + 36], eax"); // preserve argc separately from shell output + emitter.instruction("movsxd rax, eax"); // widen argc for pointer-table allocation + emitter.instruction("shl rax, 3"); // one UTF-8 pointer per argument + emitter.instruction("call __rt_heap_alloc"); // allocate persistent UTF-8 argv table + emitter.instruction("test rax, rax"); // table allocation succeeded? + emitter.instruction("jz .Linit_argv_w_table_oom"); // release shell32 array with ENOMEM + emitter.instruction("mov QWORD PTR [rsp + 48], rax"); // owned UTF-8 pointer table + emitter.instruction("mov QWORD PTR [rsp + 56], 0"); // conversion index + emitter.label(".Linit_argv_w_loop"); + emitter.instruction("mov rax, QWORD PTR [rsp + 56]"); // current index + emitter.instruction("mov ecx, DWORD PTR [rsp + 36]"); // argc + emitter.instruction("cmp rax, rcx"); // converted every argument? + emitter.instruction("jae .Linit_argv_w_done"); // publish converted argv + emitter.instruction("mov r10, QWORD PTR [rsp + 40]"); // shell32 wide argv table + emitter.instruction("mov rdi, QWORD PTR [r10 + rax * 8]"); // LPWSTR argv[index] + emitter.instruction("mov QWORD PTR [rsp + 64], rdi"); // preserve wide source across allocation + emitter.instruction("xor esi, esi"); // query UTF-8 size with no destination + emitter.instruction("xor edx, edx"); // destination capacity zero + emitter.instruction("call __rt_win_utf16_to_utf8"); // required UTF-8 bytes including NUL + emitter.instruction("test eax, eax"); // strict conversion size query succeeded? + emitter.instruction("jz .Linit_argv_w_conversion_fail"); // cleanup completed arguments + emitter.instruction("mov DWORD PTR [rsp + 32], eax"); // preserve exact byte capacity + emitter.instruction("movsxd rax, eax"); // allocation size + emitter.instruction("call __rt_heap_alloc"); // allocate persistent UTF-8 argument + emitter.instruction("test rax, rax"); // argument allocation succeeded? + emitter.instruction("jz .Linit_argv_w_arg_alloc_fail"); // cleanup completed arguments + emitter.instruction("mov r10, QWORD PTR [rsp + 48]"); // UTF-8 table + emitter.instruction("mov rcx, QWORD PTR [rsp + 56]"); // current index + emitter.instruction("mov QWORD PTR [r10 + rcx * 8], rax"); // table[index] owns UTF-8 allocation + emitter.instruction("mov rdi, QWORD PTR [rsp + 64]"); // wide source + emitter.instruction("mov rsi, rax"); // UTF-8 destination + emitter.instruction("mov edx, DWORD PTR [rsp + 32]"); // exact byte capacity + emitter.instruction("call __rt_win_utf16_to_utf8"); // strict conversion including NUL + emitter.instruction("test eax, eax"); // final conversion succeeded? + emitter.instruction("jz .Linit_argv_w_current_conversion_fail"); // free current and prior arguments + emitter.instruction("add QWORD PTR [rsp + 56], 1"); // advance converted count + emitter.instruction("jmp .Linit_argv_w_loop"); // convert next native argument + emitter.label(".Linit_argv_w_done"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 40]"); // shell32 allocation + emitter.instruction("call LocalFree"); // release UTF-16 argv array after copying + emitter.instruction("mov eax, DWORD PTR [rsp + 36]"); // clean argc + emitter.instruction("mov QWORD PTR [rip + _global_argc], rax"); // publish argc + emitter.instruction("mov rax, QWORD PTR [rsp + 48]"); // persistent UTF-8 pointer table + emitter.instruction("mov QWORD PTR [rip + _global_argv], rax"); // publish argv + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return to process bootstrap + emitter.label(".Linit_argv_w_current_conversion_fail"); + emitter.instruction("mov r10, QWORD PTR [rsp + 48]"); // UTF-8 table + emitter.instruction("mov rcx, QWORD PTR [rsp + 56]"); // current index + emitter.instruction("mov rax, QWORD PTR [r10 + rcx * 8]"); // current failed allocation + emitter.instruction("call __rt_heap_free"); // release current argument + emitter.label(".Linit_argv_w_conversion_fail"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 84"); // EILSEQ + emitter.instruction("jmp .Linit_argv_w_cleanup"); // cleanup prior arguments + emitter.label(".Linit_argv_w_arg_alloc_fail"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 12"); // ENOMEM + emitter.label(".Linit_argv_w_cleanup"); + emitter.instruction("cmp QWORD PTR [rsp + 56], 0"); // any completed argument allocations? + emitter.instruction("je .Linit_argv_w_cleanup_table"); // free pointer table + emitter.instruction("sub QWORD PTR [rsp + 56], 1"); // previous completed index + emitter.instruction("mov r10, QWORD PTR [rsp + 48]"); // UTF-8 table + emitter.instruction("mov rcx, QWORD PTR [rsp + 56]"); // index to free + emitter.instruction("mov rax, QWORD PTR [r10 + rcx * 8]"); // owned UTF-8 argument + emitter.instruction("call __rt_heap_free"); // release completed argument + emitter.instruction("jmp .Linit_argv_w_cleanup"); // continue reverse cleanup + emitter.label(".Linit_argv_w_cleanup_table"); + emitter.instruction("mov rax, QWORD PTR [rsp + 48]"); // UTF-8 pointer table + emitter.instruction("call __rt_heap_free"); // release failed table + emitter.instruction("jmp .Linit_argv_w_table_alloc_fail"); // preserve existing errno + emitter.label(".Linit_argv_w_table_oom"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 12"); // ENOMEM + emitter.label(".Linit_argv_w_table_alloc_fail"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 40]"); // shell32 UTF-16 argv allocation + emitter.instruction("call LocalFree"); // release native parsed argv + emitter.instruction("mov QWORD PTR [rip + _global_argc], 0"); // publish safe empty bootstrap state + emitter.instruction("mov QWORD PTR [rip + _global_argv], 0"); // no argv table + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return with failure state + emitter.label(".Linit_argv_w_parse_fail"); + emitter.instruction("call GetLastError"); // capture CommandLineToArgvW failure + emitter.instruction("mov DWORD PTR [rip + __rt_win32_last_error], eax"); // retain native error + emitter.instruction("call __rt_win32_errno_from_code"); // translate to POSIX errno + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish errno + emitter.instruction("mov QWORD PTR [rip + _global_argc], 0"); // safe empty argc + emitter.instruction("mov QWORD PTR [rip + _global_argv], 0"); // safe empty argv + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return to bootstrap + emitter.blank(); +} + +/// Emits the `__rt_sys_free_argv` shim that releases the tracked UTF-8 process +/// arguments allocated by [`emit_shim_sys_init_argv`]. +/// +/// The globals are cleared after releasing every string and the pointer table, +/// so process-exit paths may call the helper defensively more than once. The +/// cleanup deliberately uses the runtime heap rather than the Win32 process +/// heap because argv bootstrap allocations participate in GC/heap diagnostics. +pub(super) fn emit_shim_sys_free_argv(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_free_argv"); + emitter.instruction("sub rsp, 40"); // align nested runtime calls and reserve table/count/index locals + emitter.instruction("mov rax, QWORD PTR [rip + _global_argv]"); // load the owned UTF-8 pointer table + emitter.instruction("test rax, rax"); // was argv bootstrap initialized successfully? + emitter.instruction("jz .Lfree_argv_done"); // no table means there is nothing to release + emitter.instruction("mov QWORD PTR [rsp], rax"); // preserve table across heap-free calls + emitter.instruction("mov rax, QWORD PTR [rip + _global_argc]"); // load the number of owned argument strings + emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // preserve argc across heap-free calls + emitter.instruction("mov QWORD PTR [rsp + 16], 0"); // start at argv[0] + emitter.label(".Lfree_argv_loop"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 16]"); // current argument index + emitter.instruction("cmp rcx, QWORD PTR [rsp + 8]"); // have all owned strings been released? + emitter.instruction("jae .Lfree_argv_table"); // release the table after its entries + emitter.instruction("mov r10, QWORD PTR [rsp]"); // reload the still-live pointer table + emitter.instruction("mov rax, QWORD PTR [r10 + rcx * 8]"); // load argv[index] for the heap-free ABI + emitter.instruction("call __rt_heap_free"); // release the owned UTF-8 argument string + emitter.instruction("add QWORD PTR [rsp + 16], 1"); // advance after the clobbering runtime call + emitter.instruction("jmp .Lfree_argv_loop"); // continue through every process argument + emitter.label(".Lfree_argv_table"); + emitter.instruction("mov rax, QWORD PTR [rsp]"); // load the owned pointer table itself + emitter.instruction("call __rt_heap_free"); // release the UTF-8 pointer table last + emitter.instruction("mov QWORD PTR [rip + _global_argc], 0"); // make repeated cleanup a no-op + emitter.instruction("mov QWORD PTR [rip + _global_argv], 0"); // clear the released table pointer + emitter.label(".Lfree_argv_done"); + emitter.instruction("add rsp, 40"); // restore caller stack + emitter.instruction("ret"); // return to diagnostics or process exit + emitter.blank(); +} + +/// Emits the former in-place ANSI parser, retained only for regression reference. +#[allow(dead_code)] +fn emit_shim_sys_init_argv_legacy(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_init_argv"); + emitter.instruction("sub rsp, 40"); // shadow space (32) + alignment (8) + // -- fetch the mutable Win32 command line -- + emitter.instruction("xor eax, eax"); // dead legacy fixture intentionally owns no ANSI import + emitter.instruction("mov rsi, rax"); // rsi = cmdline cursor (preserved across Win32 calls) + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // save immutable cmdline START for pass-2 restart (pad slot above the 32-byte shadow; untouched by GetProcessHeap/HeapAlloc) + // -- pass 1: count whitespace-delimited tokens into rdi (argc) -- + emitter.instruction("xor rdi, rdi"); // rdi = argc = 0 + emitter.label(".Linit_argv_count_skip_ws"); + emitter.instruction("mov dl, BYTE PTR [rsi]"); // load next command-line byte + emitter.instruction("test dl, dl"); // check for the terminating NUL + emitter.instruction("jz .Linit_argv_count_done"); // end of command line -> stop counting + emitter.instruction("cmp dl, 32"); // is it a space? + emitter.instruction("je .Linit_argv_count_ws_adv"); // skip the space + emitter.instruction("cmp dl, 9"); // is it a tab? + emitter.instruction("je .Linit_argv_count_ws_adv"); // skip the tab + emitter.instruction("inc rdi"); // start a new token -> argc += 1 + emitter.instruction("cmp dl, 34"); // does the token open with a double quote? + emitter.instruction("jne .Linit_argv_count_unquoted"); // unquoted token -> scan to whitespace + emitter.instruction("add rsi, 1"); // skip the opening double quote + emitter.label(".Linit_argv_count_quoted_loop"); + emitter.instruction("mov dl, BYTE PTR [rsi]"); // load next quoted-token byte + emitter.instruction("test dl, dl"); // check for the terminating NUL (unbalanced quote) + emitter.instruction("jz .Linit_argv_count_done"); // unbalanced quote -> treat as end of command line + emitter.instruction("cmp dl, 34"); // look for the closing double quote + emitter.instruction("je .Linit_argv_count_quoted_end"); // closing quote found -> token ends + emitter.instruction("add rsi, 1"); // advance within the quoted token + emitter.instruction("jmp .Linit_argv_count_quoted_loop"); // continue scanning the quoted token + emitter.label(".Linit_argv_count_quoted_end"); + emitter.instruction("add rsi, 1"); // skip the closing double quote + emitter.label(".Linit_argv_count_quoted_tail"); + emitter.instruction("mov dl, BYTE PTR [rsi]"); // load the byte after the closing quote + emitter.instruction("test dl, dl"); // check for the terminating NUL + emitter.instruction("jz .Linit_argv_count_done"); // end of command line -> stop counting + emitter.instruction("cmp dl, 32"); // is it a space? + emitter.instruction("je .Linit_argv_count_ws_adv"); // whitespace after the token -> skip it + emitter.instruction("cmp dl, 9"); // is it a tab? + emitter.instruction("je .Linit_argv_count_ws_adv"); // whitespace after the token -> skip it + emitter.instruction("add rsi, 1"); // advance past any trailing non-whitespace + emitter.instruction("jmp .Linit_argv_count_quoted_tail"); // keep scanning the token tail + emitter.label(".Linit_argv_count_unquoted"); + emitter.instruction("add rsi, 1"); // advance past the first token byte + emitter.label(".Linit_argv_count_unquoted_loop"); + emitter.instruction("mov dl, BYTE PTR [rsi]"); // load next unquoted-token byte + emitter.instruction("test dl, dl"); // check for the terminating NUL + emitter.instruction("jz .Linit_argv_count_done"); // end of command line -> stop counting + emitter.instruction("cmp dl, 32"); // is it a space? + emitter.instruction("je .Linit_argv_count_ws_adv"); // whitespace -> token ends + emitter.instruction("cmp dl, 9"); // is it a tab? + emitter.instruction("je .Linit_argv_count_ws_adv"); // whitespace -> token ends + emitter.instruction("add rsi, 1"); // advance within the unquoted token + emitter.instruction("jmp .Linit_argv_count_unquoted_loop"); // continue scanning the unquoted token + emitter.label(".Linit_argv_count_ws_adv"); + emitter.instruction("add rsi, 1"); // advance past one whitespace byte + emitter.instruction("jmp .Linit_argv_count_skip_ws"); // resume whitespace skipping / token dispatch + emitter.label(".Linit_argv_count_done"); + // -- allocate the argv pointer array: HeapAlloc(GetProcessHeap(), 0, argc*8) -- + emitter.instruction("call GetProcessHeap"); // rax = default process heap (rdi/rsi preserved) + emitter.instruction("mov rcx, rax"); // rcx = heap handle (MSx64 arg1) + emitter.instruction("xor rdx, rdx"); // rdx = flags = 0 (MSx64 arg2) + emitter.instruction("mov r8, rdi"); // r8 = argc (MSx64 arg3 source) + emitter.instruction("shl r8, 3"); // r8 = argc * 8 bytes (one char* per slot) + emitter.instruction("call HeapAlloc"); // rax = argv pointer array base (rdi/rsi preserved) + emitter.instruction("mov r8, rax"); // r8 = argv array base for pass 2 + // -- pass 2: re-walk the command line, null-terminate tokens, fill argv[i] -- + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // reload cmdline START for pass 2 (rsi was consumed as the pass-1 scan cursor) + emitter.instruction("xor r9, r9"); // r9 = token index i = 0 + emitter.label(".Linit_argv_fill_skip_ws"); + emitter.instruction("mov dl, BYTE PTR [rax]"); // load next command-line byte + emitter.instruction("test dl, dl"); // check for the terminating NUL + emitter.instruction("jz .Linit_argv_fill_done"); // end of command line -> stop filling + emitter.instruction("cmp dl, 32"); // is it a space? + emitter.instruction("je .Linit_argv_fill_ws_adv"); // skip the space + emitter.instruction("cmp dl, 9"); // is it a tab? + emitter.instruction("je .Linit_argv_fill_ws_adv"); // skip the tab + emitter.instruction("cmp dl, 34"); // does the token open with a double quote? + emitter.instruction("jne .Linit_argv_fill_unquoted"); // unquoted token -> record start and scan to whitespace + emitter.instruction("lea r10, [rax + 1]"); // r10 = token start (after the opening quote, quotes stripped) + emitter.instruction("add rax, 1"); // advance past the opening double quote + emitter.label(".Linit_argv_fill_quoted_loop"); + emitter.instruction("mov dl, BYTE PTR [rax]"); // load next quoted-token byte + emitter.instruction("test dl, dl"); // check for the terminating NUL (unbalanced quote) + emitter.instruction("jz .Linit_argv_fill_store"); // unbalanced quote -> store the partial token + emitter.instruction("cmp dl, 34"); // look for the closing double quote + emitter.instruction("je .Linit_argv_fill_close_quote"); // closing quote found -> null-terminate here + emitter.instruction("add rax, 1"); // advance within the quoted token + emitter.instruction("jmp .Linit_argv_fill_quoted_loop"); // continue scanning the quoted token + emitter.label(".Linit_argv_fill_close_quote"); + emitter.instruction("mov BYTE PTR [rax], 0"); // null-terminate at the closing quote position (strip the quote) + emitter.instruction("add rax, 1"); // advance past the now-NUL position + emitter.instruction("mov QWORD PTR [r8 + r9 * 8], r10"); // argv[i] = token start pointer + emitter.instruction("add r9, 1"); // i += 1 + emitter.instruction("jmp .Linit_argv_fill_skip_ws"); // resume after the just-terminated token + emitter.label(".Linit_argv_fill_unquoted"); + emitter.instruction("mov r10, rax"); // r10 = token start (unquoted, no stripping) + emitter.instruction("add rax, 1"); // advance past the first token byte + emitter.label(".Linit_argv_fill_unquoted_loop"); + emitter.instruction("mov dl, BYTE PTR [rax]"); // load next unquoted-token byte + emitter.instruction("test dl, dl"); // check for the terminating NUL + emitter.instruction("jz .Linit_argv_fill_store"); // end of command line -> store the token + emitter.instruction("cmp dl, 32"); // is it a space? + emitter.instruction("je .Linit_argv_fill_term_unquoted"); // whitespace -> null-terminate and store + emitter.instruction("cmp dl, 9"); // is it a tab? + emitter.instruction("je .Linit_argv_fill_term_unquoted"); // whitespace -> null-terminate and store + emitter.instruction("add rax, 1"); // advance within the unquoted token + emitter.instruction("jmp .Linit_argv_fill_unquoted_loop"); // continue scanning the unquoted token + emitter.label(".Linit_argv_fill_term_unquoted"); + emitter.instruction("mov BYTE PTR [rax], 0"); // null-terminate the token at the whitespace position + emitter.instruction("mov QWORD PTR [r8 + r9 * 8], r10"); // argv[i] = token start pointer + emitter.instruction("add r9, 1"); // i += 1 + emitter.instruction("add rax, 1"); // advance past the now-NUL whitespace + emitter.instruction("jmp .Linit_argv_fill_skip_ws"); // resume after the just-terminated token + emitter.label(".Linit_argv_fill_store"); + emitter.instruction("mov QWORD PTR [r8 + r9 * 8], r10"); // argv[i] = token start pointer (command line ended at token end) + emitter.instruction("add r9, 1"); // i += 1 + emitter.instruction("jmp .Linit_argv_fill_done"); // command line exhausted -> stop filling + emitter.label(".Linit_argv_fill_ws_adv"); + emitter.instruction("add rax, 1"); // advance past one whitespace byte + emitter.instruction("jmp .Linit_argv_fill_skip_ws"); // resume whitespace skipping / token dispatch + emitter.label(".Linit_argv_fill_done"); + // -- publish the clean 64-bit argc and the argv array base to the globals -- + emitter.instruction("mov QWORD PTR [rip + _global_argc], rdi"); // _global_argc = argc (clean 64-bit count) + emitter.instruction("mov QWORD PTR [rip + _global_argv], r8"); // _global_argv = argv pointer array base + emitter.instruction("add rsp, 40"); // restore shadow space + emitter.instruction("ret"); // return to __elephc_main prologue + emitter.blank(); +} + +/// Emits a shim that converts `mmap` to `VirtualAlloc`. +/// +/// SysV: rdi=addr, rsi=len, rdx=prot, r10=flags, r8=fd, r9=offset +pub(super) fn emit_shim_mmap(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_mmap"); + emitter.instruction("sub rsp, 40"); // shadow space + emitter.instruction("mov rcx, rdi"); // base address (NULL = let OS choose) + emitter.instruction("mov rdx, rsi"); // size + emitter.instruction("mov r8, 0x3000"); // MEM_RESERVE | MEM_COMMIT for a protectable standalone mapping + emitter.instruction("mov r9, 0x04"); // PAGE_READWRITE (default) + emitter.instruction("call VirtualAlloc"); // allocate memory + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return base address in rax + emitter.blank(); +} + +/// Emits a shim that converts `munmap` to `VirtualFree`. +pub(super) fn emit_shim_munmap(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_munmap"); + emitter.instruction("sub rsp, 40"); // shadow space + emitter.instruction("mov rcx, rdi"); // base address + emitter.instruction("xor rdx, rdx"); // size = 0 (MEM_RELEASE requires 0) + emitter.instruction("mov r8, 0x8000"); // MEM_RELEASE + emitter.instruction("call VirtualFree"); // free memory + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits a shim that provides a simple heap allocation via `HeapAlloc`. +/// +/// SysV: rdi=size → returns pointer +pub(super) fn emit_shim_brk(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_brk"); + emitter.instruction("sub rsp, 40"); // shadow space + emitter.instruction("call GetProcessHeap"); // get default heap handle + emitter.instruction("mov rcx, rax"); // heap handle + emitter.instruction("xor rdx, rdx"); // flags = 0 + emitter.instruction("mov r8, rdi"); // size + emitter.instruction("call HeapAlloc"); // allocate from heap + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return pointer + emitter.blank(); +} + +/// Emits a shim that returns the current process ID. +pub(super) fn emit_shim_getpid(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_getpid"); + emitter.instruction("sub rsp, 40"); // shadow space + emitter.instruction("call GetCurrentProcessId"); // get PID + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return PID in rax + emitter.blank(); +} + +/// Emits a shim that generates random bytes via `BCryptGenRandom`. +/// +/// SysV: rdi=buffer, rsi=count. At most `u32::MAX` bytes are filled per call; +/// callers use the getrandom partial-read contract to request the remaining bytes. +/// BCryptGenRandom's signature is `BCryptGenRandom(hAlgorithm, pbBuffer, cbBuffer, +/// dwFlags)`, so it is called with `hAlgorithm = NULL`, `pbBuffer = buffer`, +/// `cbBuffer = count`, and `dwFlags = BCRYPT_USE_SYSTEM_PREFERRED_RNG (2)`. It +/// returns STATUS_SUCCESS (0) on success, a nonzero NTSTATUS on failure. This shim +/// mirrors Linux getrandom's contract: it returns the byte count on success and -1 on +/// error, so callers can treat a negative return as a hard failure. +pub(super) fn emit_shim_getrandom(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_getrandom"); + emitter.instruction("sub rsp, 40"); // shadow(32) + padding + emitter.instruction("mov rax, 4294967295"); // BCryptGenRandom cbBuffer is an unsigned 32-bit length + emitter.instruction("cmp rsi, rax"); // does the request exceed one BCryptGenRandom call? + emitter.instruction("cmova rsi, rax"); // cap this partial fill; the caller loops for the remainder + emitter.instruction("mov QWORD PTR [rsp + 32], rsi"); // save the actual byte count to return on success + emitter.instruction("xor rcx, rcx"); // hAlgorithm = NULL (use the system-preferred RNG) + emitter.instruction("mov rdx, rdi"); // pbBuffer = caller buffer + emitter.instruction("mov r8, rsi"); // cbBuffer = caller-requested byte count + emitter.instruction("mov r9, 2"); // dwFlags = BCRYPT_USE_SYSTEM_PREFERRED_RNG + emitter.instruction("call BCryptGenRandom"); // fill the whole buffer with CSPRNG bytes + emitter.instruction("test rax, rax"); // BCryptGenRandom returns STATUS_SUCCESS (0) on success + emitter.instruction("jnz .Lgetrandom_fail"); // any nonzero NTSTATUS → return -1 + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // return the byte count on success + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lgetrandom_fail"); + emitter.instruction("mov rax, -1"); // return -1 on failure + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Symbols in the libm math/FP family — `pow` and the trig/exp/log cluster — +/// each routed to a dedicated `__rt_sys_` shim on windows-x86_64 (W3c). +/// Unlike the other Class-1 shims above, every one of these functions takes +/// its arguments and returns its result in **floating-point** registers, and +/// that register assignment is IDENTICAL between SysV and MSx64: 1st/2nd FP +/// arg in xmm0/xmm1, result in xmm0, in BOTH ABIs. So none of these shims +/// perform a register shuffle — the sole ABI difference a bare `call ` +/// violates is the MSx64 caller contract of 32-byte shadow space + 16-byte +/// stack alignment at the call site, which every shim below supplies via +/// [`emit_fp_shadow_shim`]. +const MATH_FP_SHIM_SYMBOLS: &[&str] = &[ + "pow", "sin", "cos", "tan", "asin", "acos", "atan", "sinh", "cosh", "tanh", "exp", "log", + "log2", "log10", "atan2", "hypot", "fmod", "round", +]; + +/// Emits the uniform FP-shadow-space shim body for `symbol`, under the label +/// `__rt_sys_`: +/// ```text +/// __rt_sys_: +/// sub rsp, 40 ; 32B shadow space + 8B realignment +/// call ; xmm0/xmm1 args, xmm0 result — untouched, identical in both ABIs +/// add rsp, 40 +/// ret +/// ``` +/// `sub rsp, 40` reserves the mandatory 32-byte MSx64 shadow space plus 8 +/// bytes to restore 16-byte alignment at the nested `call`: this shim is +/// entered with `rsp ≡ 8 (mod 16)` (the post-`call` value on entry to any +/// x86_64 function, per the SysV/Win64-shared `call`-pushes-a-return-address +/// convention every caller in this runtime already honors), so after +/// `sub rsp, 40` (also ≡ 0 mod 8, and 40 ≡ 8 mod 16) `rsp` lands exactly on a +/// 16-byte boundary for `call `. xmm0/xmm1 (arguments) and xmm0 +/// (return value) are never touched — libm functions read/write those exact +/// registers under both ABIs, so no shuffle is needed, only the shadow space. +fn emit_fp_shadow_shim(emitter: &mut Emitter, symbol: &str) { + emitter.label_global(&format!("__rt_sys_{}", symbol)); + emitter.instruction("sub rsp, 40"); // 32B shadow space + 8B realignment + emitter.instruction(&format!("call {}", symbol)); // FP args/result in xmm0/xmm1 — untouched, identical in both ABIs + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits all math/libm FP shims (see [`MATH_FP_SHIM_SYMBOLS`] / +/// [`emit_fp_shadow_shim`]). +pub(super) fn emit_shim_math_fp(emitter: &mut Emitter) { + for symbol in MATH_FP_SHIM_SYMBOLS { + emit_fp_shadow_shim(emitter, symbol); + } +} + +/// Emits the `__rt_sys_strtod` shim: converts SysV `strtod(s, endptr)` to MSx64 and calls +/// msvcrt `strtod`. SysV: rdi=s, rsi=endptr → MSx64: rcx=s, rdx=endptr. The double return +/// stays in xmm0 (same in both ABIs). The runtime emitters call this shim on Windows-x86_64 +/// instead of `call strtod` directly, so the msvcrt import sees MSx64-shaped arguments. +pub(super) fn emit_shim_strtod(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_strtod"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov rdx, rsi"); // endptr → arg2 (rdx) FIRST, before rdi is moved + emitter.instruction("mov rcx, rdi"); // s → arg1 (rcx) + emitter.instruction("call strtod"); // msvcrt strtod (returns double in xmm0) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits the `__rt_sys_strtol` shim: converts SysV `strtol(s, endptr, base)` to MSx64 and +/// calls msvcrt `strtol`. SysV: rdi=s, rsi=endptr, rdx=base → MSx64: rcx=s, rdx=endptr, +/// r8=base. Windows `long` is 32-bit, so the msvcrt return in `eax` is +/// sign-extended before SysV-staged callers consume it as a 64-bit integer. +/// rdx is saved to r8 BEFORE rdx is overwritten, per the socket-shim idiom at +/// `emit_shim_socket_shims`. +pub(super) fn emit_shim_strtol(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_strtol"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov r8, rdx"); // base → arg3 (r8) FIRST, save rdx before overwrite + emitter.instruction("mov rdx, rsi"); // endptr → arg2 (rdx) + emitter.instruction("mov rcx, rdi"); // s → arg1 (rcx) + emitter.instruction("call strtol"); // msvcrt strtol (returns long in rax) + emitter.instruction("cdqe"); // sign-extend Windows' 32-bit long into the runtime's 64-bit result + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits the `__rt_sys_snprintf` shim: converts the SysV variadic +/// `snprintf(buf, size, fmt, precision, double)` call to the MSx64 variadic ABI and calls +/// MinGW's standards-conforming `__mingw_snprintf`. SysV variadic: +/// rdi=buf, rsi=size, rdx=fmt, rcx=precision, xmm0=double, +/// `al`=1 (vector-register count). MSx64 variadic: rcx=buf, rdx=size, +/// r8=fmt, r9=precision, 5th arg (double) at `[rsp+32]` (the slot above the +/// 32-byte shadow), `al` ignored. +/// +/// Register-shuffle hazard: SysV arg4 (precision) is in `rcx`, which is ALSO MSx64 arg1, so +/// precision is saved to `r10` BEFORE `rcx` is overwritten. SysV arg3 (fmt) is in `rdx`, which +/// is ALSO MSx64 arg2, so it is moved to `r8` FIRST (per the socket-shim idiom). The double +/// is spilled to the 5th-arg stack slot via `movsd`; it is NOT passed in a GPR. `al` is left +/// MinGW's formatter is used instead of msvcrt's legacy formatter because PHP +/// preserves the complete binary-double tail for high requested precisions. +/// The int return stays in rax. +pub(super) fn emit_shim_snprintf(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_snprintf"); + emitter.instruction("sub rsp, 56"); // shadow(32) + 5th-arg slot(8) + pad(16), 16-byte aligned + emitter.instruction("mov r10, rcx"); // SAVE precision (SysV arg4) before rcx is overwritten + emitter.instruction("mov r8, rdx"); // fmt → arg3 (r8) FIRST, save rdx before overwrite + emitter.instruction("mov rdx, rsi"); // size → arg2 (rdx) + emitter.instruction("mov rcx, rdi"); // buf → arg1 (rcx) + emitter.instruction("mov r9, r10"); // precision → arg4 (r9) + emitter.instruction("movsd QWORD PTR [rsp + 32], xmm0"); // double → 5th arg (stack slot above shadow) + emitter.instruction("call __mingw_snprintf"); // render through MinGW's standards-conforming formatter and return the byte count in rax + emitter.instruction("add rsp, 56"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits the `__rt_sys_snprintf_double` shim: converts a SysV variadic +/// `snprintf(buf, size, fmt, double)` call — exactly ONE variadic argument, a +/// `double`, with NO leading integer precision argument — to the MSx64 variadic +/// ABI and calls MinGW's standards-conforming `__mingw_snprintf`. Used by +/// `__rt_sprintf`'s `%f`/`%e`/`%g`/`%E`/ +/// `%G` path (`runtime::strings::sprintf_x86_64`), where the format string +/// already embeds the user's precision/width/flags, unlike `__rt_number_format`, +/// `__rt_ftoa`, and `__rt_json_ftoa`'s +/// `snprintf(buf, size, "%.*e", precision, double)` shape (handled by +/// [`emit_shim_snprintf`]) whose double is logically the 5th argument. +/// +/// SysV variadic: rdi=buf, rsi=size, rdx=fmt, xmm0=double, `al`=1. +/// MSx64 variadic: rcx=buf, rdx=size, r8=fmt, and the double is positional +/// argument 4 — under the MSx64 variadic-call convention a floating-point +/// argument in one of the first four positions MUST be duplicated into BOTH its +/// positional integer register (`r9`) and its positional xmm register (`xmm3`): +/// the callee's prologue home-spills `rcx`/`rdx`/`r8`/`r9` into the va_list +/// backing store regardless of type, so a `va_arg(double)` read of position 4 +/// pulls from `r9`'s home slot, not `xmm3`, unless both are populated. (This is +/// the "windows garbage output" root cause: [`emit_shim_snprintf`]'s shim +/// treats a caller's `rcx` as a leading precision int and pushes the double to +/// the 5th-argument stack slot at `[rsp+32]`, which msvcrt never reads for a +/// 3-fixed-arg call — it reads uninitialized `r9`/stack garbage instead.) +pub(super) fn emit_shim_snprintf_double(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_snprintf_double"); + emitter.instruction("sub rsp, 40"); // shadow(32) + pad(8), 16-byte aligned (entry rsp is 8 mod 16) + emitter.instruction("mov r8, rdx"); // fmt → arg3 (r8) FIRST, save rdx before overwrite + emitter.instruction("mov rdx, rsi"); // size → arg2 (rdx) + emitter.instruction("mov rcx, rdi"); // buf → arg1 (rcx) + emitter.instruction("movq r9, xmm0"); // double raw bits → arg4 integer register (r9), required for MSx64 variadic va_arg + emitter.instruction("movaps xmm3, xmm0"); // double → arg4 float register (xmm3), duplicate of r9's bits + emitter.instruction("call __mingw_snprintf"); // render through MinGW's standards-conforming formatter and return the byte count in rax + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits a shim that converts `ioctl` to `ioctlsocket`. +/// +/// `ioctlsocket` returns a 32-bit `int` status in `eax` (0 on success, `SOCKET_ERROR` = -1 +/// on failure) — reached from PHP's `stream_set_blocking()` via `__rt_sys_fcntl`'s +/// `F_SETFL`/`FIONBIO` delegation (fcntl syscall 72 → `__rt_sys_fcntl` → tail-calls here). +/// `stream_set_blocking.rs:94/114` sign-tests the result (`test rax,rax; js`), so without +/// sign-extension a failure leaves `rax = 0x00000000_FFFFFFFF` (positive) and the failure +/// is missed. `cdqe` restores the 64-bit negative. +pub(super) fn emit_shim_ioctl(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_ioctl"); + emitter.instruction("sub rsp, 40"); // shadow space + emitter.instruction("mov r8, rdx"); // save argp before rdx is overwritten + emitter.instruction("mov rcx, rdi"); // socket + emitter.instruction("mov rdx, rsi"); // cmd + emitter.instruction("call ioctlsocket"); // ioctl socket + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je .Lioctl_capture_errno"); // publish Winsock failure + emitter.instruction("cdqe"); // sign-extend eax → rax (SOCKET_ERROR=-1 negative) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lioctl_capture_errno"); + emitter.instruction("call __rt_wsa_capture_errno"); // capture WSAGetLastError and return -1 + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return SOCKET_ERROR + emitter.blank(); +} + +/// Emits dup/dup2 shims using msvcrt `_dup`/`_dup2`. +/// +/// Both `_dup`/`_dup2` return a 32-bit `int` in `eax` (the new fd, or -1 on failure) — the +/// same int-status shape as the Winsock shims, so `cdqe` sign-extends a -1 failure into a +/// 64-bit negative for any sign-testing caller. Note: `opendir_glob.rs`'s `dup(2)` call +/// (the historical justification for this class of fix) is a direct native `call dup`, not +/// routed through this shim or the syscall-number transform, so it is unaffected either way. +pub(super) fn emit_shim_dup_shims(emitter: &mut Emitter) { + // _dup(fd) → returns new fd or -1 + emitter.label_global("__rt_sys_dup"); + emitter.instruction("sub rsp, 40"); // shadow space + emitter.instruction("mov rcx, rdi"); // fd + emitter.instruction("call _dup"); // msvcrt _dup + emitter.instruction("cdqe"); // sign-extend eax → rax (-1 failure negative) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return new fd + emitter.blank(); + // _dup2(fd, fd2) → returns fd2 or -1 + emitter.label_global("__rt_sys_dup2"); + emitter.instruction("sub rsp, 40"); // shadow space + emitter.instruction("mov rcx, rdi"); // fd + emitter.instruction("mov rdx, rsi"); // fd2 + emitter.instruction("call _dup2"); // msvcrt _dup2 + emitter.instruction("cdqe"); // sign-extend eax → rax (-1 failure negative) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return new fd + emitter.blank(); +} + +/// Emits getuid/getgid (return 0, PHP behavior on Windows) and setuid/setgid/getppid/ +/// getpriority/setpriority (return -1, ENOSYS — POSIX-only functions). +pub(super) fn emit_shim_getuid_shims(emitter: &mut Emitter) { + // getuid/getgid: return 0 on Windows (PHP behavior — no Unix UID/GID) + for (label, _desc) in &[ + ("__rt_sys_getuid", "getuid"), + ("__rt_sys_getgid", "getgid"), + ] { + emitter.label_global(label); + emitter.instruction("xor rax, rax"); // return 0 (PHP behavior on Windows) + emitter.instruction("ret"); // return + emitter.blank(); + } + // setuid/setgid/getppid/getpriority/setpriority: return -1 (ENOSYS) + for (label, _desc) in &[ + ("__rt_sys_setuid", "setuid"), + ("__rt_sys_setgid", "setgid"), + ("__rt_sys_getppid", "getppid"), + ("__rt_sys_getpriority", "getpriority"), + ("__rt_sys_setpriority", "setpriority"), + ] { + emitter.label_global(label); + emitter.instruction("mov rax, -1"); // return -1 (not supported on Windows) + emitter.instruction("ret"); // return + emitter.blank(); + } +} + +/// Emits a kill shim using OpenProcess+TerminateProcess for SIGKILL, -1 otherwise. +/// +/// SysV: rdi=pid, rsi=signal. Windows has no `posix_kill` equivalent for +/// arbitrary signals, so silently reporting success for a signal that was +/// never delivered is worse than a loud failure: both the OpenProcess-failed +/// path and the unsupported-signal path now return -1 (POSIX failure) +/// instead of 0. `posix_kill`/`__rt_sys_kill` has no PHP-reachable caller in +/// this runtime today (grepped: no `posix_kill` lowering, no raw `syscall` +/// site loads `eax` with 62, and no C-symbol `call kill` site exists outside +/// this shim's own delegate) and signal 0 (the POSIX existence-check idiom) +/// has no consumer either, so it is treated the same as any other +/// unsupported signal rather than implemented as an existence check. +pub(super) fn emit_shim_kill(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_kill"); + emitter.instruction("cmp rsi, 9"); // sig == SIGKILL? + emitter.instruction("jne .Lsys_kill_noop"); // skip if not SIGKILL + emitter.instruction("sub rsp, 40"); // shadow(32) + handle(8) + emitter.instruction("mov rcx, 1"); // dwDesiredAccess = PROCESS_TERMINATE + emitter.instruction("xor rdx, rdx"); // bInheritHandle = FALSE + emitter.instruction("mov r8, rdi"); // dwProcessId = pid + emitter.instruction("call OpenProcess"); // open process handle + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // save handle + emitter.instruction("test rax, rax"); // check if OpenProcess succeeded + emitter.instruction("je .Lsys_kill_fail"); // jump if failed + emitter.instruction("mov rcx, rax"); // handle + emitter.instruction("mov rdx, 1"); // exit code + emitter.instruction("call TerminateProcess"); // terminate process + emitter.instruction("mov rcx, QWORD PTR [rsp + 32]"); // reload handle + emitter.instruction("call CloseHandle"); // close handle + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("xor rax, rax"); // return 0 (SIGKILL delivered) + emitter.instruction("ret"); // return + emitter.label(".Lsys_kill_fail"); + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("mov rax, -1"); // OpenProcess failed -> return -1 + emitter.instruction("ret"); // return + emitter.label(".Lsys_kill_noop"); + emitter.instruction("mov rax, -1"); // return -1 (unsupported signal on Windows; no silent success) + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits a Win32-backed `uname` shim filling the flat Linux-layout `struct +/// utsname` buffer (`char[5][65]` = 325 bytes, UPWARD: sysname@+0, nodename@+65, +/// release@+130, version@+195, machine@+260 — matches `php_uname.rs`'s +/// `uts_field_len`/`uts_offset`, both 65 bytes/field on Windows). +/// +/// SysV: rdi=utsname buffer (MSx64 non-volatile, survives every Win32 call +/// below; copied to rsi up front since rdi is consumed as the `rep stosb` +/// zero-fill destination). Replaces the previous `call uname` stub, which +/// self-recursed: msvcrt exposes no `uname`, so the local `uname` C-symbol +/// delegate (this file) forwarded back into `__rt_sys_uname`, which called +/// `uname` again, forever. +/// +/// `sysname`/`nodename`/`machine` are real (literal, `GetComputerNameW`, +/// `GetNativeSystemInfo`). `release`/`version` report a minimal-but-correct +/// fallback ("0.0"/"build 0") rather than the true OS version: the accurate +/// source, `RtlGetVersion`, lives in `ntdll.dll`, which is not part of this +/// target's link set (`src/linker.rs`) and out of scope for a shim-only change. +pub(super) fn emit_shim_uname(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_uname"); + emitter.instruction("sub rsp, 232"); // shadow, size, 64-WCHAR hostname, SYSTEM_INFO, saved uts base + emitter.instruction("mov rsi, rdi"); // preserve the utsname buffer base (rsi survives every Win32 call below) + emitter.instruction("mov QWORD PTR [rsp + 224], rsi"); // preserve base across UTF conversion argument setup + emitter.instruction("cld"); // forward direction for rep stosb + emitter.instruction("xor eax, eax"); // zero fill byte + emitter.instruction("mov ecx, 325"); // sizeof flat utsname buffer: 5 fields * 65 bytes + emitter.instruction("rep stosb"); // zero the whole utsname buffer before filling any field + // -- sysname: literal "Windows NT" -- + // x86-64 has no `mov m64, imm64` encoding (only `mov r64, imm64` then + // store), so the 8-byte "Windows " literal is staged through r10 first — + // mirrors the heap-kind stamping pattern in tempnam.rs. + emitter.instruction("mov r10, 0x2073776F646E6957"); // "Windows " + emitter.instruction("mov QWORD PTR [rsi], r10"); // NUL already zeroed by rep stosb + emitter.instruction("mov WORD PTR [rsi + 8], 0x544E"); // "NT" + // -- nodename: GetComputerNameW to a wide temporary, then strict UTF-8 -- + emitter.instruction("mov DWORD PTR [rsp + 32], 64"); // lpnSize in/out: buffer capacity in TCHARs + emitter.instruction("lea rcx, [rsp + 40]"); // wide hostname temporary + emitter.instruction("lea rdx, [rsp + 32]"); // lpnSize + emitter.instruction("call GetComputerNameW"); // retrieve Unicode computer name + emitter.instruction("test eax, eax"); // hostname query succeeded? + emitter.instruction("jz .Luname_nodename_done"); // leave zeroed nodename on failure + emitter.instruction("lea rdi, [rsp + 40]"); // UTF-16 hostname source + emitter.instruction("mov rsi, QWORD PTR [rsp + 224]"); // utsname buffer base + emitter.instruction("add rsi, 65"); // UTF-8 nodename destination + emitter.instruction("mov rdx, 65"); // nodename field byte capacity + emitter.instruction("call __rt_win_utf16_to_utf8"); // strict hostname conversion + emitter.label(".Luname_nodename_done"); + emitter.instruction("mov rsi, QWORD PTR [rsp + 224]"); // restore utsname base + // -- release/version: minimal-but-correct fallback (see docblock) -- + emitter.instruction("mov DWORD PTR [rsi + 130], 0x00302E30"); // release = "0.0" + emitter.instruction("mov r10, 0x3020646C697562"); // "build 0" (8-byte literal, staged through r10 — see above) + emitter.instruction("mov QWORD PTR [rsi + 195], r10"); // version = "build 0" + // -- machine: GetNativeSystemInfo(&SYSTEM_INFO) — wProcessorArchitecture is the first WORD -- + emitter.instruction("lea rcx, [rsp + 168]"); // lpSystemInfo = &SYSTEM_INFO + emitter.instruction("call GetNativeSystemInfo"); // fill wProcessorArchitecture at offset 0 + emitter.instruction("movzx eax, WORD PTR [rsp + 168]"); // wProcessorArchitecture + emitter.instruction("cmp eax, 9"); // PROCESSOR_ARCHITECTURE_AMD64? + emitter.instruction("je .Luname_machine_amd64"); // → "AMD64" + emitter.instruction("cmp eax, 12"); // PROCESSOR_ARCHITECTURE_ARM64? + emitter.instruction("je .Luname_machine_arm64"); // → "ARM64" + emitter.instruction("mov DWORD PTR [rsi + 260], 0x00363878"); // fallback machine = "x86" (also PROCESSOR_ARCHITECTURE_INTEL) + emitter.instruction("jmp .Luname_done"); // → done + emitter.label(".Luname_machine_amd64"); + emitter.instruction("mov DWORD PTR [rsi + 260], 0x36444D41"); // "AMD6" + emitter.instruction("mov WORD PTR [rsi + 264], 0x0034"); // "4\0" -> "AMD64" + emitter.instruction("jmp .Luname_done"); // → done + emitter.label(".Luname_machine_arm64"); + emitter.instruction("mov DWORD PTR [rsi + 260], 0x364D5241"); // "ARM6" + emitter.instruction("mov WORD PTR [rsi + 264], 0x0034"); // "4\0" -> "ARM64" + emitter.label(".Luname_done"); + emitter.instruction("xor eax, eax"); // return 0 (success) + emitter.instruction("add rsp, 232"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits a sysinfo shim using GlobalMemoryStatusEx for memory info. +/// +/// `GlobalMemoryStatusEx(MEMORYSTATUSEX* lpBuffer)` requires the caller to set +/// `lpBuffer->dwLength` (the first DWORD, offset 0) to `sizeof(MEMORYSTATUSEX)` +/// (64) BEFORE the call; otherwise it fails with `ERROR_INVALID_PARAMETER` on +/// every invocation. The shim initializes `dwLength` and honors the returned +/// BOOL (nonzero = success) instead of unconditionally reporting success, +/// translating it to the POSIX `sysinfo(2)` convention this shim's callers +/// expect: 0 on success, -1 on failure. +pub(super) fn emit_shim_sysinfo(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_sysinfo"); + emitter.instruction("sub rsp, 40"); // shadow space + emitter.instruction("mov rcx, rdi"); // sysinfo struct pointer + emitter.instruction("mov DWORD PTR [rcx], 64"); // initialise MEMORYSTATUSEX.dwLength before GlobalMemoryStatusEx + emitter.instruction("call GlobalMemoryStatusEx"); // get memory status; BOOL result in eax + emitter.instruction("add rsp, 40"); // restore stack (before the branch, so both paths are balanced) + emitter.instruction("test eax, eax"); // GlobalMemoryStatusEx returns BOOL (nonzero = success) + emitter.instruction("jnz .Lsysinfo_ok"); // success -> return 0 + emitter.instruction("mov rax, -1"); // failure -> return -1 + emitter.instruction("ret"); // return + emitter.label(".Lsysinfo_ok"); + emitter.instruction("xor eax, eax"); // return 0 (success) + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits an execve shim using msvcrt _execvp. +/// +/// SysV: rdi=path, rsi=argv, rdx=envp (ignored on Windows). +pub(super) fn emit_shim_execve(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_execve"); + emitter.instruction("sub rsp, 40"); // shadow space + emitter.instruction("mov rcx, rdi"); // path + emitter.instruction("mov rdx, rsi"); // argv + emitter.instruction("call _execvp"); // execute program (replaces process) + emitter.instruction("add rsp, 40"); // restore stack (only reached on failure) + emitter.instruction("ret"); // return -1 on failure (rax from _execvp) + emitter.blank(); +} + +/// Emits a loud-failure futex shim for the unsupported Linux-only syscall. +/// +/// Returning success here would let raw-syscall callers assume that they slept +/// or woke a waiter when no synchronization happened. Windows runtime paths +/// must use their native synchronization primitives instead. +pub(super) fn emit_shim_futex(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_futex"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 38"); // ENOSYS: no synchronization was performed + emitter.instruction("mov rax, -1"); // report the unsupported operation instead of false success + emitter.instruction("ret"); // return the POSIX failure sentinel + emitter.blank(); +} + +/// Emits an mprotect shim using VirtualProtect. +pub(super) fn emit_shim_mprotect(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_mprotect"); + emitter.instruction("sub rsp, 40"); // shadow(32) + old_protect(8) + emitter.instruction("mov r8, 0x04"); // default nonzero POSIX protections to PAGE_READWRITE + emitter.instruction("test edx, edx"); // distinguish PROT_NONE from readable/writable mappings + emitter.instruction("jnz .Lrt_sys_mprotect_protection_ready"); // keep PAGE_READWRITE for every supported nonzero protection mask + emitter.instruction("mov r8, 0x01"); // translate PROT_NONE to PAGE_NOACCESS for guard pages + emitter.label(".Lrt_sys_mprotect_protection_ready"); + emitter.instruction("mov rcx, rdi"); // address + emitter.instruction("mov rdx, rsi"); // size + emitter.instruction("lea r9, [rsp + 32]"); // &old_protect + emitter.instruction("call VirtualProtect"); // change protection + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); +} diff --git a/src/codegen_support/runtime/win32/shims_net.rs b/src/codegen_support/runtime/win32/shims_net.rs new file mode 100644 index 0000000000..7256cab7ad --- /dev/null +++ b/src/codegen_support/runtime/win32/shims_net.rs @@ -0,0 +1,1943 @@ +//! Win32 shims for the socket/DNS/hostname family: gethostname, +//! gethostbyname, socket/winsock init/cleanup, accept4/setsockopt/getsockopt/ +//! socketpair/pselect6/sendmsg/recvmsg, and the W3e-2 net/dns/inet + misc +//! msvcrt family (getaddrinfo, inet_pton/ntop, strtoll, atof, setlocale, chown failure). + +use crate::codegen::emit::Emitter; + +/// Legacy AF_UNIX loopback registry retained out of the Windows runtime build. +#[cfg(any())] +pub(super) fn emit_unix_loopback_registry(emitter: &mut Emitter) { + emitter.label_global("__rt_win_unix_find_socket"); + emitter.instruction("lea rax, [rip + _win_unix_socket_records]"); // first socket record + emitter.instruction("mov ecx, 32"); // bounded registry capacity + emitter.label(".Lwin_unix_find_socket_loop"); + emitter.instruction("cmp QWORD PTR [rax], rdi"); // record owns this SOCKET? + emitter.instruction("je .Lwin_unix_find_socket_done"); // return matching record + emitter.instruction("add rax, 32"); // advance to next socket record + emitter.instruction("dec ecx"); // one fewer record remains + emitter.instruction("jnz .Lwin_unix_find_socket_loop"); // scan remaining records + emitter.instruction("xor eax, eax"); // no matching socket record + emitter.label(".Lwin_unix_find_socket_done"); + emitter.instruction("ret"); // return record or NULL + emitter.blank(); + + emitter.label_global("__rt_win_unix_alloc_socket"); + emitter.instruction("lea rax, [rip + _win_unix_socket_records]"); // first socket record + emitter.instruction("mov ecx, 32"); // bounded registry capacity + emitter.label(".Lwin_unix_alloc_socket_loop"); + emitter.instruction("cmp QWORD PTR [rax], 0"); // free socket record? + emitter.instruction("je .Lwin_unix_alloc_socket_store"); // initialize this record + emitter.instruction("add rax, 32"); // advance to next socket record + emitter.instruction("dec ecx"); // one fewer record remains + emitter.instruction("jnz .Lwin_unix_alloc_socket_loop"); // scan remaining records + emitter.instruction("xor eax, eax"); // registry exhausted + emitter.instruction("ret"); // return NULL + emitter.label(".Lwin_unix_alloc_socket_store"); + emitter.instruction("mov QWORD PTR [rax], rdi"); // store full-width SOCKET + emitter.instruction("mov QWORD PTR [rax + 8], rsi"); // preserve SOCK_STREAM/SOCK_DGRAM + emitter.instruction("mov QWORD PTR [rax + 16], 0"); // no bound local endpoint yet + emitter.instruction("mov QWORD PTR [rax + 24], 0"); // no connected peer endpoint yet + emitter.instruction("ret"); // return initialized record + emitter.blank(); + + emitter.label_global("__rt_win_unix_close_all"); + emitter.instruction("sub rsp, 72"); // shadow space and bounded registry-loop locals + emitter.instruction("lea rax, [rip + _win_unix_socket_records]"); // first emulated socket record + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // retain current record across closesocket + emitter.instruction("mov DWORD PTR [rsp + 40], 32"); // bounded socket registry capacity + emitter.label(".Lwin_unix_close_all_socket_loop"); + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // current socket record + emitter.instruction("mov rcx, QWORD PTR [rax]"); // registered full-width SOCKET + emitter.instruction("test rcx, rcx"); // occupied record? + emitter.instruction("jz .Lwin_unix_close_all_socket_next"); // skip unused record + emitter.instruction("call closesocket"); // release the native loopback socket + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // recover record after the Win32 call + emitter.instruction("mov QWORD PTR [rax], 0"); // release socket metadata + emitter.instruction("mov QWORD PTR [rax + 16], 0"); // detach local endpoint metadata + emitter.instruction("mov QWORD PTR [rax + 24], 0"); // detach peer endpoint metadata + emitter.label(".Lwin_unix_close_all_socket_next"); + emitter.instruction("add QWORD PTR [rsp + 32], 32"); // advance to next socket record + emitter.instruction("dec DWORD PTR [rsp + 40]"); // one fewer record remains + emitter.instruction("jnz .Lwin_unix_close_all_socket_loop"); // close every occupied record + emitter.instruction("lea rax, [rip + _win_unix_endpoint_records]"); // first named endpoint record + emitter.instruction("mov ecx, 32"); // bounded endpoint registry capacity + emitter.label(".Lwin_unix_close_all_endpoint_loop"); + emitter.instruction("mov QWORD PTR [rax], 0"); // unregister endpoint name + emitter.instruction("mov QWORD PTR [rax + 8], 0"); // detach endpoint owner socket + emitter.instruction("add rax, 140"); // advance to next endpoint record + emitter.instruction("dec ecx"); // one fewer endpoint remains + emitter.instruction("jnz .Lwin_unix_close_all_endpoint_loop"); // clear every endpoint record + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return after deterministic teardown + emitter.blank(); + + emitter.label_global("__rt_win_unix_find_path"); + emitter.instruction("lea rax, [rip + _win_unix_endpoint_records]"); // first endpoint record + emitter.instruction("mov r8d, 32"); // bounded registry capacity + emitter.label(".Lwin_unix_find_path_record"); + emitter.instruction("cmp QWORD PTR [rax], 0"); // active endpoint? + emitter.instruction("je .Lwin_unix_find_path_next"); // skip unused record + emitter.instruction("lea r9, [rax + 32]"); // stored path bytes + emitter.instruction("mov ecx, 108"); // sockaddr_un path capacity + emitter.instruction("mov r10, rdi"); // requested NUL-terminated path + emitter.label(".Lwin_unix_find_path_compare"); + emitter.instruction("mov dl, BYTE PTR [r10]"); // requested byte + emitter.instruction("cmp dl, BYTE PTR [r9]"); // same as stored byte? + emitter.instruction("jne .Lwin_unix_find_path_next"); // path differs + emitter.instruction("test dl, dl"); // reached matching terminator? + emitter.instruction("jz .Lwin_unix_find_path_done"); // return matching endpoint + emitter.instruction("inc r10"); // next requested byte + emitter.instruction("inc r9"); // next stored byte + emitter.instruction("dec ecx"); // one fewer byte remains + emitter.instruction("jnz .Lwin_unix_find_path_compare"); // compare bounded path + emitter.instruction("jmp .Lwin_unix_find_path_done"); // 108-byte paths match + emitter.label(".Lwin_unix_find_path_next"); + emitter.instruction("add rax, 140"); // advance to next endpoint record + emitter.instruction("dec r8d"); // one fewer endpoint remains + emitter.instruction("jnz .Lwin_unix_find_path_record"); // scan remaining endpoints + emitter.instruction("xor eax, eax"); // path is not registered + emitter.label(".Lwin_unix_find_path_done"); + emitter.instruction("ret"); // return endpoint or NULL + emitter.blank(); + + emitter.label_global("__rt_win_unix_find_port"); + emitter.instruction("lea rax, [rip + _win_unix_endpoint_records]"); // first endpoint record + emitter.instruction("mov ecx, 32"); // bounded registry capacity + emitter.label(".Lwin_unix_find_port_loop"); + emitter.instruction("cmp QWORD PTR [rax], 0"); // active endpoint? + emitter.instruction("je .Lwin_unix_find_port_next"); // skip unused record + emitter.instruction("cmp WORD PTR [rax + 24], di"); // same network-order loopback port? + emitter.instruction("je .Lwin_unix_find_port_done"); // return matching endpoint + emitter.label(".Lwin_unix_find_port_next"); + emitter.instruction("add rax, 140"); // advance to next endpoint record + emitter.instruction("dec ecx"); // one fewer endpoint remains + emitter.instruction("jnz .Lwin_unix_find_port_loop"); // scan remaining endpoints + emitter.instruction("xor eax, eax"); // port is not registered + emitter.label(".Lwin_unix_find_port_done"); + emitter.instruction("ret"); // return endpoint or NULL + emitter.blank(); + + emitter.label_global("__rt_win_unix_alloc_endpoint"); + emitter.instruction("lea rax, [rip + _win_unix_endpoint_records]"); // first endpoint record + emitter.instruction("mov ecx, 32"); // bounded registry capacity + emitter.label(".Lwin_unix_alloc_endpoint_loop"); + emitter.instruction("cmp QWORD PTR [rax], 0"); // free endpoint record? + emitter.instruction("je .Lwin_unix_alloc_endpoint_store"); // initialize this record + emitter.instruction("add rax, 140"); // advance to next endpoint record + emitter.instruction("dec ecx"); // one fewer endpoint remains + emitter.instruction("jnz .Lwin_unix_alloc_endpoint_loop"); // scan remaining endpoints + emitter.instruction("xor eax, eax"); // registry exhausted + emitter.instruction("ret"); // return NULL + emitter.label(".Lwin_unix_alloc_endpoint_store"); + emitter.instruction("mov QWORD PTR [rax], 1"); // mark endpoint active + emitter.instruction("mov QWORD PTR [rax + 8], rdi"); // owner SOCKET + emitter.instruction("mov QWORD PTR [rax + 16], rsi"); // socket type + emitter.instruction("mov QWORD PTR [rax + 24], rdx"); // network-order loopback port + emitter.instruction("lea r8, [rax + 32]"); // endpoint path destination + emitter.instruction("xor ecx, ecx"); // path byte index + emitter.label(".Lwin_unix_alloc_endpoint_copy"); + emitter.instruction("mov r9b, BYTE PTR [r10 + rcx]"); // source path byte + emitter.instruction("mov BYTE PTR [r8 + rcx], r9b"); // copy path byte + emitter.instruction("test r9b, r9b"); // copied terminator? + emitter.instruction("jz .Lwin_unix_alloc_endpoint_done"); // endpoint is complete + emitter.instruction("inc ecx"); // next path byte + emitter.instruction("cmp ecx, 107"); // reserve final byte for NUL + emitter.instruction("jb .Lwin_unix_alloc_endpoint_copy"); // keep copying bounded path + emitter.instruction("mov BYTE PTR [r8 + 107], 0"); // terminate truncated path deterministically + emitter.label(".Lwin_unix_alloc_endpoint_done"); + emitter.instruction("ret"); // return endpoint record + emitter.blank(); + + emitter.label_global("__rt_win_unix_unlink_path"); + emitter.instruction("sub rsp, 8"); // align internal call + emitter.instruction("call __rt_win_unix_find_path"); // locate emulated endpoint by path + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("test rax, rax"); // registered endpoint? + emitter.instruction("jz .Lwin_unix_unlink_path_missing"); // let filesystem unlink handle it + emitter.instruction("mov QWORD PTR [rax], 0"); // release endpoint name + emitter.instruction("mov eax, 1"); // handled successfully + emitter.instruction("ret"); // return true + emitter.label(".Lwin_unix_unlink_path_missing"); + emitter.instruction("xor eax, eax"); // not an emulated endpoint + emitter.instruction("ret"); // return false + emitter.blank(); + + emitter.label_global("__rt_win_unix_write_sockaddr"); + emitter.instruction("mov r8, rdi"); // endpoint record, or NULL for anonymous peer + emitter.instruction("mov r9, rsi"); // destination sockaddr_un + emitter.instruction("mov r10, rdx"); // destination length pointer + emitter.instruction("xor eax, eax"); // zero fill value + emitter.instruction("mov ecx, 110"); // family plus sun_path bytes + emitter.instruction("mov rdi, r9"); // memset destination + emitter.instruction("rep stosb"); // clear synthesized sockaddr_un + emitter.instruction("mov WORD PTR [r9], 1"); // AF_UNIX in shared layout + emitter.instruction("test r8, r8"); // named endpoint available? + emitter.instruction("jz .Lwin_unix_write_sockaddr_length"); // anonymous peer has empty path + emitter.instruction("lea rsi, [r8 + 32]"); // stored endpoint path + emitter.instruction("lea rdi, [r9 + 2]"); // sockaddr_un sun_path + emitter.instruction("mov ecx, 108"); // bounded path capacity + emitter.label(".Lwin_unix_write_sockaddr_copy"); + emitter.instruction("mov al, BYTE PTR [rsi]"); // stored path byte + emitter.instruction("mov BYTE PTR [rdi], al"); // copy into sockaddr_un + emitter.instruction("inc rsi"); // next source byte + emitter.instruction("inc rdi"); // next destination byte + emitter.instruction("test al, al"); // copied terminator? + emitter.instruction("jz .Lwin_unix_write_sockaddr_length"); // finish synthesized address + emitter.instruction("dec ecx"); // one fewer byte remains + emitter.instruction("jnz .Lwin_unix_write_sockaddr_copy"); // copy bounded path + emitter.label(".Lwin_unix_write_sockaddr_length"); + emitter.instruction("test r10, r10"); // caller supplied addrlen pointer? + emitter.instruction("jz .Lwin_unix_write_sockaddr_done"); // no length to publish + emitter.instruction("mov DWORD PTR [r10], 110"); // synthesized sockaddr_un size + emitter.label(".Lwin_unix_write_sockaddr_done"); + emitter.instruction("xor eax, eax"); // successful status + emitter.instruction("ret"); // return success + emitter.blank(); +} + +/// Emits stateless compatibility labels for obsolete AF_UNIX registry callers. +/// +/// PHP does not register AF_UNIX transports on Windows. The socket creation +/// shim rejects the family before these labels can be reached; `unlink` and +/// process cleanup may still call their no-op entry points. +pub(super) fn emit_unix_loopback_registry(emitter: &mut Emitter) { + for label in [ + "__rt_win_unix_find_socket", + "__rt_win_unix_alloc_socket", + "__rt_win_unix_find_path", + "__rt_win_unix_find_port", + "__rt_win_unix_alloc_endpoint", + "__rt_win_unix_unlink_path", + ] { + emitter.label_global(label); + emitter.instruction("xor eax, eax"); // no Windows AF_UNIX registry entry exists + emitter.instruction("ret"); // report absent metadata or an unhandled path + emitter.blank(); + } + emitter.label_global("__rt_win_unix_close_all"); + emitter.instruction("ret"); // no AF_UNIX compatibility sockets are registered + emitter.blank(); + emitter.label_global("__rt_win_unix_write_sockaddr"); + emitter.instruction("mov rax, -1"); // AF_UNIX address synthesis is unavailable on Windows + emitter.instruction("ret"); // report unsupported address synthesis + emitter.blank(); +} + +/// Emits a shim that wraps msvcrt `gethostname`. +pub(super) fn emit_shim_gethostname(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_gethostname"); + emitter.instruction("sub rsp, 40"); // shadow space + emitter.instruction("mov rcx, rdi"); // buffer + emitter.instruction("mov rdx, rsi"); // length + emitter.instruction("call gethostname"); // msvcrt gethostname + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je .Lgethostname_capture_errno"); // publish Winsock hostname failure + emitter.instruction("cdqe"); // sign-extend status + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lgethostname_capture_errno"); + emitter.instruction("call __rt_wsa_capture_errno"); // capture WSAGetLastError and return -1 + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return SOCKET_ERROR + emitter.blank(); +} + +/// Emits the `__rt_sys_gethostbyname` shim: converts SysV `gethostbyname(name)` to MSx64 +/// and calls ws2_32 `gethostbyname`. SysV: rdi=name → MSx64: rcx=name. Mirrors +/// `emit_shim_gethostname` (1-arg case). The `struct hostent *` return stays in rax. +pub(super) fn emit_shim_gethostbyname(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_gethostbyname"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov rcx, rdi"); // name → arg1 (rcx) + emitter.instruction("call gethostbyname"); // ws2_32 gethostbyname (returns struct hostent* in rax) + emitter.instruction("test rax, rax"); // lookup succeeded? + emitter.instruction("jz .Lgethostbyname_capture_errno"); // publish resolver failure + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lgethostbyname_capture_errno"); + emitter.instruction("call __rt_wsa_capture_errno"); // capture WSAGetLastError + emitter.instruction("xor eax, eax"); // pointer API failure sentinel is NULL + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return NULL + emitter.blank(); +} + +/// Emits socket-related shims (socket, connect, bind, listen, accept, send, recv, etc.). +/// sendto/recvfrom have 6 args and are emitted separately with dedicated shims. +/// +/// Class-3 sign-extension rule: EVERY Winsock shim that returns a 32-bit `int` STATUS in +/// `eax` (0 on success, `SOCKET_ERROR` = -1 = 0xFFFFFFFF on failure) whose consumer +/// sign-tests the 64-bit `rax` needs `cdqe` after the call. On x86_64 writing `eax` zeroes +/// the upper 32 bits of `rax`, so without sign-extension a failure leaves +/// `rax = 0x00000000_FFFFFFFF` (positive) and a `test rax,rax; js` / `cmp rax,0; jl` +/// consumer misses it. `bind`, `listen`, `connect`, `shutdown`, `getsockname`, and +/// `getpeername` all return int status and are therefore emitted OUTSIDE the shared loop as +/// dedicated `cdqe` blocks. Verified consumers: +/// - bind: stream_socket_server.rs:394/406, stream_socket_server_v6.rs:375/387, +/// unix_socket_server.rs (`test rax,rax; js`) +/// - listen: same server sites as bind +/// - connect: stream_socket_client.rs:380-381 (`test rax,rax; js`) +/// - shutdown: stream_socket_shutdown.rs:47 (`test rax,rax; js` — a failed shutdown +/// otherwise reports true) +/// - getsockname: stream_socket_get_name.rs:310 (`cmp rax,0; jl` — a failed getsockname +/// otherwise parses an uninitialized sockaddr) +/// - getpeername: same shared `__rt_ssgn_after_x86` check (routed via syscall 52) +/// +/// ONLY `socket` and `accept` stay in the shared loop below: they return a `SOCKET` (a +/// 64-bit `UINT_PTR` handle, NOT an int status), where `INVALID_SOCKET` = ~0 is already a +/// 64-bit -1 and `cdqe` would CORRUPT a valid handle whose bit 31 is set. +pub(super) fn emit_shim_socket_shims(emitter: &mut Emitter) { + // Windows assigns AF_INET6=23 while the shared runtime emits Linux's value 10. + emitter.label_global("__rt_sys_socket"); + emitter.instruction("sub rsp, 40"); // shadow space and ABI alignment + emitter.instruction("cmp edi, 1"); // shared AF_UNIX requested? + emitter.instruction("je .Lsocket_unix_unsupported"); // PHP does not register AF_UNIX transports on Windows + emitter.instruction("mov r8, rdx"); // preserve protocol before loading MSx64 arg2 + emitter.instruction("mov ecx, edi"); // address family + emitter.instruction("cmp ecx, 10"); // Linux AF_INET6? + emitter.instruction("jne .Lsocket_family_ready"); // other family values already agree + emitter.instruction("mov ecx, 23"); // Winsock AF_INET6 + emitter.label(".Lsocket_family_ready"); + emitter.instruction("mov edx, esi"); // socket type + emitter.instruction("call socket"); // create full-width Winsock SOCKET + emitter.instruction("cmp rax, -1"); // INVALID_SOCKET? + emitter.instruction("je .Lsocket_capture_errno"); // publish ordinary socket failure + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return SOCKET + emitter.label(".Lsocket_unix_unsupported"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 97"); // EAFNOSUPPORT, matching PHP's absent Windows transport + emitter.instruction("mov rax, -1"); // INVALID_SOCKET + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // reject AF_UNIX before consulting Winsock + emitter.label(".Lsocket_capture_errno"); + emitter.instruction("call __rt_wsa_capture_errno"); // translate Winsock failure + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return INVALID_SOCKET + emitter.blank(); + + // The AF_UNIX registry branches in the generic accept/bind/connect/name/ + // send/receive shims below are legacy-unreachable: `__rt_sys_socket` + // rejects family 1 before a Windows socket can acquire registry metadata. + // They remain emitted for now because removing them from this combined + // multi-shim function is a larger mechanical cleanup than the parity fix. + let shims: &[(&str, &str)] = &[("__rt_sys_accept", "accept")]; + for (label, func) in shims { + emitter.label_global(label); + emitter.instruction("sub rsp, 88"); // shadow space plus Unix-emulation locals + emitter.instruction("mov QWORD PTR [rsp + 40], rsi"); // preserve caller address buffer + emitter.instruction("mov QWORD PTR [rsp + 48], rdx"); // preserve caller length pointer + emitter.instruction("mov QWORD PTR [rsp + 56], rdi"); // preserve listening socket + emitter.instruction("call __rt_win_unix_find_socket"); // detect emulated listener + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // retain listener metadata or NULL + emitter.instruction("mov r8, rdx"); // save arg3 before rdx is overwritten + emitter.instruction("mov rcx, QWORD PTR [rsp + 56]"); // listening socket + emitter.instruction("mov rdx, QWORD PTR [rsp + 40]"); // caller address buffer + emitter.instruction("mov r8, QWORD PTR [rsp + 48]"); // caller address length pointer + emitter.instruction(&format!("call {}", func)); // call Win32 function (returns 64-bit SOCKET handle) + emitter.instruction("cmp rax, -1"); // INVALID_SOCKET? + emitter.instruction(&format!("je .L{func}_capture_errno")); // publish the corresponding Winsock failure + emitter.instruction("cmp QWORD PTR [rsp + 32], 0"); // emulated Unix listener? + emitter.instruction(&format!("je .L{func}_success")); // native accept is complete + emitter.instruction("mov QWORD PTR [rsp + 64], rax"); // preserve accepted socket + emitter.instruction("mov rdi, rax"); // accepted socket for registry + emitter.instruction("mov r10, QWORD PTR [rsp + 32]"); // listener metadata + emitter.instruction("mov rsi, QWORD PTR [r10 + 8]"); // inherit socket type + emitter.instruction("call __rt_win_unix_alloc_socket"); // register accepted Unix stream + emitter.instruction("test rax, rax"); // metadata allocation succeeded? + emitter.instruction(&format!("jz .L{func}_registry_full")); // close accepted socket on exhaustion + emitter.instruction("mov r10, QWORD PTR [rsp + 32]"); // listener metadata + emitter.instruction("mov r11, QWORD PTR [r10 + 16]"); // listener local endpoint + emitter.instruction("mov QWORD PTR [rax + 16], r11"); // accepted socket shares local name + emitter.instruction("mov QWORD PTR [rax + 24], 0"); // client peer is anonymous + emitter.instruction("xor edi, edi"); // anonymous Unix peer endpoint + emitter.instruction("mov rsi, QWORD PTR [rsp + 40]"); // caller sockaddr buffer + emitter.instruction("mov rdx, QWORD PTR [rsp + 48]"); // caller length pointer + emitter.instruction("test rsi, rsi"); // caller requested peer address? + emitter.instruction(&format!("jz .L{func}_restore_socket")); // no sockaddr to synthesize + emitter.instruction("call __rt_win_unix_write_sockaddr"); // publish anonymous AF_UNIX peer + emitter.label(&format!(".L{func}_restore_socket")); + emitter.instruction("mov rax, QWORD PTR [rsp + 64]"); // restore accepted socket + emitter.label(&format!(".L{func}_success")); + emitter.instruction("add rsp, 88"); // restore stack + emitter.instruction("ret"); // return handle (no cdqe: 64-bit SOCKET) + emitter.label(&format!(".L{func}_registry_full")); + emitter.instruction("mov rcx, QWORD PTR [rsp + 64]"); // untracked accepted socket + emitter.instruction("call closesocket"); // release native resource + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 24"); // EMFILE registry-capacity failure + emitter.instruction("mov rax, -1"); // INVALID_SOCKET + emitter.instruction("add rsp, 88"); // restore stack + emitter.instruction("ret"); // return failure + emitter.label(&format!(".L{func}_capture_errno")); + emitter.instruction("call __rt_wsa_capture_errno"); // capture WSAGetLastError and return -1 + emitter.instruction("add rsp, 88"); // restore stack + emitter.instruction("ret"); // return INVALID_SOCKET + emitter.blank(); + } + // bind: sign-extend the Winsock int return into rax so SOCKET_ERROR (-1) reads as + // a 64-bit negative and the `test rax,rax; js fail` consumer detects a failed bind. + emitter.label_global("__rt_sys_bind"); + emitter.instruction("sub rsp, 120"); // shadow space, native sockaddr, and emulation locals + emitter.instruction("mov QWORD PTR [rsp + 32], rsi"); // preserve sockaddr for family restoration + emitter.instruction("mov QWORD PTR [rsp + 40], 0"); // family is untranslated by default + emitter.instruction("mov QWORD PTR [rsp + 48], rdi"); // preserve full-width socket + emitter.instruction("mov QWORD PTR [rsp + 56], rdx"); // preserve caller sockaddr length + emitter.instruction("call __rt_win_unix_find_socket"); // detect emulated Unix socket + emitter.instruction("mov QWORD PTR [rsp + 88], rax"); // retain socket metadata or NULL + emitter.instruction("test rax, rax"); // emulated socket? + emitter.instruction("jz .Lbind_native_family"); // use ordinary bind path + emitter.instruction("mov r10, QWORD PTR [rsp + 32]"); // caller sockaddr_un + emitter.instruction("cmp WORD PTR [r10], 1"); // AF_UNIX address? + emitter.instruction("jne .Lbind_invalid_unix_family"); // reject incoherent emulated bind + emitter.instruction("lea rdi, [r10 + 2]"); // requested Unix path + emitter.instruction("call __rt_win_unix_find_path"); // reject duplicate active endpoint names + emitter.instruction("test rax, rax"); // path already registered? + emitter.instruction("jnz .Lbind_address_in_use"); // deterministic EADDRINUSE + emitter.instruction("pxor xmm0, xmm0"); // zero native sockaddr_in + emitter.instruction("movdqu XMMWORD PTR [rsp + 64], xmm0"); // clear 16-byte address + emitter.instruction("mov WORD PTR [rsp + 64], 2"); // Winsock AF_INET + emitter.instruction("mov DWORD PTR [rsp + 68], 0x0100007f"); // 127.0.0.1 in network byte order + emitter.instruction("mov rcx, QWORD PTR [rsp + 48]"); // socket + emitter.instruction("lea rdx, [rsp + 64]"); // loopback sockaddr_in + emitter.instruction("mov r8d, 16"); // sockaddr_in length + emitter.instruction("call bind"); // bind ephemeral loopback port + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je .Lbind_capture_errno"); // publish native bind failure + emitter.instruction("mov DWORD PTR [rsp + 80], 16"); // getsockname input capacity + emitter.instruction("mov rcx, QWORD PTR [rsp + 48]"); // bound socket + emitter.instruction("lea rdx, [rsp + 64]"); // receive assigned loopback port + emitter.instruction("lea r8, [rsp + 80]"); // sockaddr length pointer + emitter.instruction("call getsockname"); // discover assigned ephemeral port + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je .Lbind_capture_errno"); // publish discovery failure + emitter.instruction("mov rax, QWORD PTR [rsp + 88]"); // socket metadata + emitter.instruction("mov rdi, QWORD PTR [rsp + 48]"); // endpoint owner socket + emitter.instruction("mov rsi, QWORD PTR [rax + 8]"); // endpoint stream/datagram type + emitter.instruction("movzx edx, WORD PTR [rsp + 66]"); // assigned network-order port + emitter.instruction("mov r10, QWORD PTR [rsp + 32]"); // original sockaddr_un + emitter.instruction("add r10, 2"); // Unix path bytes + emitter.instruction("call __rt_win_unix_alloc_endpoint"); // publish path-to-loopback mapping + emitter.instruction("test rax, rax"); // endpoint capacity available? + emitter.instruction("jz .Lbind_registry_full"); // deterministic capacity failure + emitter.instruction("mov r10, QWORD PTR [rsp + 88]"); // socket metadata + emitter.instruction("mov QWORD PTR [r10 + 16], rax"); // attach local endpoint + emitter.instruction("xor eax, eax"); // bind success + emitter.instruction("add rsp, 120"); // restore stack + emitter.instruction("ret"); // return success + emitter.label(".Lbind_native_family"); + emitter.instruction("mov rsi, QWORD PTR [rsp + 32]"); // restore caller sockaddr + emitter.instruction("cmp WORD PTR [rsi], 10"); // Linux AF_INET6 sockaddr? + emitter.instruction("jne .Lbind_family_ready"); // no translation needed + emitter.instruction("mov WORD PTR [rsi], 23"); // Winsock AF_INET6 + emitter.instruction("mov QWORD PTR [rsp + 40], 1"); // restore shared layout after the call + emitter.label(".Lbind_family_ready"); + emitter.instruction("mov r8, QWORD PTR [rsp + 56]"); // caller sockaddr length + emitter.instruction("mov rcx, QWORD PTR [rsp + 48]"); // socket + emitter.instruction("mov rdx, QWORD PTR [rsp + 32]"); // output sockaddr + emitter.instruction("call bind"); // call Winsock bind (result in eax) + emitter.instruction("cmp QWORD PTR [rsp + 40], 0"); // translated sockaddr family? + emitter.instruction("je .Lbind_family_restored"); // keep original family + emitter.instruction("mov r10, QWORD PTR [rsp + 32]"); // shared sockaddr pointer + emitter.instruction("mov WORD PTR [r10], 10"); // restore Linux AF_INET6 + emitter.label(".Lbind_family_restored"); + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je .Lbind_capture_errno"); // publish Winsock failure + emitter.instruction("cdqe"); // sign-extend eax → rax (SOCKET_ERROR=-1 negative) + emitter.instruction("add rsp, 120"); // restore stack + emitter.instruction("ret"); // return sign-extended result + emitter.label(".Lbind_invalid_unix_family"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 97"); // EAFNOSUPPORT + emitter.instruction("jmp .Lbind_local_failure"); // return failure + emitter.label(".Lbind_address_in_use"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 98"); // EADDRINUSE + emitter.instruction("jmp .Lbind_local_failure"); // return failure + emitter.label(".Lbind_registry_full"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 28"); // ENOSPC registry-capacity failure + emitter.label(".Lbind_local_failure"); + emitter.instruction("mov rax, -1"); // SOCKET_ERROR + emitter.instruction("add rsp, 120"); // restore stack + emitter.instruction("ret"); // return failure + emitter.label(".Lbind_capture_errno"); + emitter.instruction("call __rt_wsa_capture_errno"); // capture WSAGetLastError and return -1 + emitter.instruction("add rsp, 120"); // restore stack + emitter.instruction("ret"); // return SOCKET_ERROR + emitter.blank(); + // listen: sign-extend the Winsock int return into rax so SOCKET_ERROR (-1) reads as + // a 64-bit negative and the `test rax,rax; js fail` consumer detects a failed listen. + emitter.label_global("__rt_sys_listen"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov r8, rdx"); // save arg3 before rdx is overwritten + emitter.instruction("mov rcx, rdi"); // arg1 + emitter.instruction("mov rdx, rsi"); // backlog + emitter.instruction("call listen"); // call Winsock listen (result in eax) + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je .Llisten_capture_errno"); // publish Winsock failure + emitter.instruction("cdqe"); // sign-extend eax → rax (SOCKET_ERROR=-1 negative) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return sign-extended result + emitter.label(".Llisten_capture_errno"); + emitter.instruction("call __rt_wsa_capture_errno"); // capture WSAGetLastError and return -1 + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return SOCKET_ERROR + emitter.blank(); + // connect: sign-extend the Winsock int return into rax so SOCKET_ERROR (-1) reads as + // a 64-bit negative and the `test rax,rax; js fail` consumer detects a refused port. + emitter.label_global("__rt_sys_connect"); + emitter.instruction("sub rsp, 120"); // shadow space, native sockaddr, and emulation locals + emitter.instruction("mov QWORD PTR [rsp + 32], rsi"); // preserve sockaddr for family restoration + emitter.instruction("mov QWORD PTR [rsp + 40], 0"); // family is untranslated by default + emitter.instruction("mov QWORD PTR [rsp + 48], rdi"); // preserve full-width socket + emitter.instruction("mov QWORD PTR [rsp + 56], rdx"); // preserve caller sockaddr length + emitter.instruction("call __rt_win_unix_find_socket"); // detect emulated Unix socket + emitter.instruction("mov QWORD PTR [rsp + 88], rax"); // retain socket metadata or NULL + emitter.instruction("test rax, rax"); // emulated socket? + emitter.instruction("jz .Lconnect_native_family"); // use ordinary connect path + emitter.instruction("mov r10, QWORD PTR [rsp + 32]"); // caller sockaddr_un + emitter.instruction("cmp WORD PTR [r10], 1"); // AF_UNIX address? + emitter.instruction("jne .Lconnect_invalid_unix_family"); // reject incoherent emulated connect + emitter.instruction("lea rdi, [r10 + 2]"); // requested Unix path + emitter.instruction("call __rt_win_unix_find_path"); // resolve path to loopback endpoint + emitter.instruction("test rax, rax"); // endpoint exists? + emitter.instruction("jz .Lconnect_path_missing"); // deterministic ENOENT + emitter.instruction("mov QWORD PTR [rsp + 96], rax"); // retain peer endpoint + emitter.instruction("mov r10, QWORD PTR [rsp + 88]"); // socket metadata + emitter.instruction("mov r11, QWORD PTR [r10 + 8]"); // client socket type + emitter.instruction("cmp r11, QWORD PTR [rax + 16]"); // same as endpoint type? + emitter.instruction("jne .Lconnect_type_mismatch"); // deterministic EPROTOTYPE + emitter.instruction("pxor xmm0, xmm0"); // zero native sockaddr_in + emitter.instruction("movdqu XMMWORD PTR [rsp + 64], xmm0"); // clear 16-byte address + emitter.instruction("mov WORD PTR [rsp + 64], 2"); // Winsock AF_INET + emitter.instruction("mov r11w, WORD PTR [rax + 24]"); // registered network-order port + emitter.instruction("mov WORD PTR [rsp + 66], r11w"); // sockaddr_in port + emitter.instruction("mov DWORD PTR [rsp + 68], 0x0100007f"); // 127.0.0.1 in network byte order + emitter.instruction("mov rcx, QWORD PTR [rsp + 48]"); // socket + emitter.instruction("lea rdx, [rsp + 64]"); // resolved loopback sockaddr_in + emitter.instruction("mov r8d, 16"); // sockaddr_in length + emitter.instruction("call connect"); // connect emulated Unix endpoint + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je .Lconnect_capture_errno"); // publish native connect failure + emitter.instruction("mov r10, QWORD PTR [rsp + 88]"); // socket metadata + emitter.instruction("mov r11, QWORD PTR [rsp + 96]"); // resolved peer endpoint + emitter.instruction("mov QWORD PTR [r10 + 24], r11"); // attach peer name + emitter.instruction("xor eax, eax"); // connect success + emitter.instruction("add rsp, 120"); // restore stack + emitter.instruction("ret"); // return success + emitter.label(".Lconnect_native_family"); + emitter.instruction("mov rsi, QWORD PTR [rsp + 32]"); // restore caller sockaddr + emitter.instruction("cmp WORD PTR [rsi], 10"); // Linux AF_INET6 sockaddr? + emitter.instruction("jne .Lconnect_family_ready"); // no translation needed + emitter.instruction("mov WORD PTR [rsi], 23"); // Winsock AF_INET6 + emitter.instruction("mov QWORD PTR [rsp + 40], 1"); // restore shared layout after the call + emitter.label(".Lconnect_family_ready"); + emitter.instruction("mov r8, QWORD PTR [rsp + 56]"); // caller sockaddr length + emitter.instruction("mov rcx, QWORD PTR [rsp + 48]"); // socket + emitter.instruction("mov rdx, rsi"); // arg2 + emitter.instruction("call connect"); // call Winsock connect (result in eax) + emitter.instruction("cmp QWORD PTR [rsp + 40], 0"); // translated sockaddr family? + emitter.instruction("je .Lconnect_family_restored"); // keep original family + emitter.instruction("mov r10, QWORD PTR [rsp + 32]"); // shared sockaddr pointer + emitter.instruction("mov WORD PTR [r10], 10"); // restore Linux AF_INET6 + emitter.label(".Lconnect_family_restored"); + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je .Lconnect_capture_errno"); // publish refused/timeout errors + emitter.instruction("cdqe"); // sign-extend eax → rax (SOCKET_ERROR=-1 negative) + emitter.instruction("add rsp, 120"); // restore stack + emitter.instruction("ret"); // return sign-extended result + emitter.label(".Lconnect_invalid_unix_family"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 97"); // EAFNOSUPPORT + emitter.instruction("jmp .Lconnect_local_failure"); // return failure + emitter.label(".Lconnect_path_missing"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 2"); // ENOENT + emitter.instruction("jmp .Lconnect_local_failure"); // return failure + emitter.label(".Lconnect_type_mismatch"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 91"); // EPROTOTYPE + emitter.label(".Lconnect_local_failure"); + emitter.instruction("mov rax, -1"); // SOCKET_ERROR + emitter.instruction("add rsp, 120"); // restore stack + emitter.instruction("ret"); // return failure + emitter.label(".Lconnect_capture_errno"); + emitter.instruction("call __rt_wsa_capture_errno"); // capture WSAGetLastError and return -1 + emitter.instruction("add rsp, 120"); // restore stack + emitter.instruction("ret"); // return SOCKET_ERROR + emitter.blank(); + // shutdown: sign-extend the Winsock int return into rax so SOCKET_ERROR (-1) reads as + // a 64-bit negative and the `test rax,rax; js fail` consumer detects a failed shutdown. + emitter.label_global("__rt_sys_shutdown"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov r8, rdx"); // save arg3 before rdx is overwritten + emitter.instruction("mov rcx, rdi"); // arg1 + emitter.instruction("mov rdx, rsi"); // arg2 + emitter.instruction("call shutdown"); // call Winsock shutdown (result in eax) + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je .Lshutdown_capture_errno"); // publish Winsock failure + emitter.instruction("cdqe"); // sign-extend eax → rax (SOCKET_ERROR=-1 negative) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return sign-extended result + emitter.label(".Lshutdown_capture_errno"); + emitter.instruction("call __rt_wsa_capture_errno"); // capture WSAGetLastError and return -1 + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return SOCKET_ERROR + emitter.blank(); + // getsockname: sign-extend the Winsock int return into rax so SOCKET_ERROR (-1) reads as + // a 64-bit negative and the `cmp rax,0; jl fail` consumer detects a failed getsockname. + emitter.label_global("__rt_sys_getsockname"); + emitter.instruction("sub rsp, 72"); // shadow space plus sockaddr and emulation locals + emitter.instruction("mov QWORD PTR [rsp + 32], rsi"); // preserve output sockaddr + emitter.instruction("mov QWORD PTR [rsp + 40], rdx"); // preserve output length pointer + emitter.instruction("mov QWORD PTR [rsp + 48], rdi"); // preserve full-width socket + emitter.instruction("call __rt_win_unix_find_socket"); // detect emulated Unix socket + emitter.instruction("test rax, rax"); // emulated socket metadata? + emitter.instruction("jz .Lgetsockname_native"); // query Winsock for ordinary socket + emitter.instruction("mov rdi, QWORD PTR [rax + 16]"); // local endpoint or NULL + emitter.instruction("mov rsi, QWORD PTR [rsp + 32]"); // caller sockaddr buffer + emitter.instruction("mov rdx, QWORD PTR [rsp + 40]"); // caller length pointer + emitter.instruction("call __rt_win_unix_write_sockaddr"); // synthesize AF_UNIX local name + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return success + emitter.label(".Lgetsockname_native"); + emitter.instruction("mov r8, QWORD PTR [rsp + 40]"); // output length pointer + emitter.instruction("mov rcx, QWORD PTR [rsp + 48]"); // socket + emitter.instruction("mov rdx, QWORD PTR [rsp + 32]"); // output sockaddr + emitter.instruction("call getsockname"); // call Winsock getsockname (result in eax) + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je .Lgetsockname_capture_errno"); // publish Winsock failure + emitter.instruction("mov r10, QWORD PTR [rsp + 32]"); // output sockaddr + emitter.instruction("cmp WORD PTR [r10], 23"); // Winsock AF_INET6? + emitter.instruction("jne .Lgetsockname_family_ready"); // other families already agree + emitter.instruction("mov WORD PTR [r10], 10"); // restore shared Linux AF_INET6 layout + emitter.label(".Lgetsockname_family_ready"); + emitter.instruction("cdqe"); // sign-extend eax → rax (SOCKET_ERROR=-1 negative) + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return sign-extended result + emitter.label(".Lgetsockname_capture_errno"); + emitter.instruction("call __rt_wsa_capture_errno"); // capture WSAGetLastError and return -1 + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return SOCKET_ERROR + emitter.blank(); + // getpeername: sign-extend the Winsock int return into rax so SOCKET_ERROR (-1) reads as + // a 64-bit negative and the `cmp rax,0; jl fail` consumer detects a failed getpeername. + emitter.label_global("__rt_sys_getpeername"); + emitter.instruction("sub rsp, 72"); // shadow space plus sockaddr and emulation locals + emitter.instruction("mov QWORD PTR [rsp + 32], rsi"); // preserve output sockaddr + emitter.instruction("mov QWORD PTR [rsp + 40], rdx"); // preserve output length pointer + emitter.instruction("mov QWORD PTR [rsp + 48], rdi"); // preserve full-width socket + emitter.instruction("call __rt_win_unix_find_socket"); // detect emulated Unix socket + emitter.instruction("test rax, rax"); // emulated socket metadata? + emitter.instruction("jz .Lgetpeername_native"); // query Winsock for ordinary socket + emitter.instruction("mov rdi, QWORD PTR [rax + 24]"); // peer endpoint or NULL for anonymous peer + emitter.instruction("mov rsi, QWORD PTR [rsp + 32]"); // caller sockaddr buffer + emitter.instruction("mov rdx, QWORD PTR [rsp + 40]"); // caller length pointer + emitter.instruction("call __rt_win_unix_write_sockaddr"); // synthesize AF_UNIX peer name + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return success + emitter.label(".Lgetpeername_native"); + emitter.instruction("mov r8, QWORD PTR [rsp + 40]"); // output length pointer + emitter.instruction("mov rcx, QWORD PTR [rsp + 48]"); // socket + emitter.instruction("mov rdx, QWORD PTR [rsp + 32]"); // output sockaddr + emitter.instruction("call getpeername"); // call Winsock getpeername (result in eax) + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je .Lgetpeername_capture_errno"); // publish Winsock failure + emitter.instruction("mov r10, QWORD PTR [rsp + 32]"); // output sockaddr + emitter.instruction("cmp WORD PTR [r10], 23"); // Winsock AF_INET6? + emitter.instruction("jne .Lgetpeername_family_ready"); // other families already agree + emitter.instruction("mov WORD PTR [r10], 10"); // restore shared Linux AF_INET6 layout + emitter.label(".Lgetpeername_family_ready"); + emitter.instruction("cdqe"); // sign-extend eax → rax (SOCKET_ERROR=-1 negative) + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return sign-extended result + emitter.label(".Lgetpeername_capture_errno"); + emitter.instruction("call __rt_wsa_capture_errno"); // capture WSAGetLastError and return -1 + emitter.instruction("add rsp, 72"); // restore stack + emitter.instruction("ret"); // return SOCKET_ERROR + emitter.blank(); + // closesocket: 1 arg + emitter.label_global("__rt_sys_closesocket"); + emitter.instruction("sub rsp, 56"); // shadow space plus socket local + emitter.instruction("mov QWORD PTR [rsp + 32], rdi"); // preserve full-width socket + emitter.instruction("mov rcx, rdi"); // socket + emitter.instruction("call closesocket"); // close socket + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je .Lclosesocket_capture_errno"); // publish Winsock failure + emitter.instruction("mov rdi, QWORD PTR [rsp + 32]"); // closed socket for metadata lookup + emitter.instruction("call __rt_win_unix_find_socket"); // locate emulation metadata + emitter.instruction("test rax, rax"); // emulated socket record? + emitter.instruction("jz .Lclosesocket_done"); // no metadata to clear + emitter.instruction("mov QWORD PTR [rax], 0"); // release socket record + emitter.label(".Lclosesocket_done"); + emitter.instruction("xor eax, eax"); // successful status + emitter.instruction("add rsp, 56"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lclosesocket_capture_errno"); + emitter.instruction("call __rt_wsa_capture_errno"); // capture WSAGetLastError and return -1 + emitter.instruction("add rsp, 56"); // restore stack + emitter.instruction("ret"); // return SOCKET_ERROR + emitter.blank(); + + // sendto: 6 args — socket, buf, len, flags, dest_addr, addrlen + // SysV: rdi=socket, rsi=buf, rdx=len, r10=flags, r8=dest_addr, r9=addrlen + // MSx64: rcx, rdx, r8, r9, [rsp+32], [rsp+40] + // Winsock sendto returns the byte count (>= 0) or SOCKET_ERROR (-1) as a 32-bit int; + // stream_socket_sendto.rs:415 sign-tests it (`cmp rax,0; jl`), so cdqe sign-extends a + // -1 failure into a 64-bit negative instead of a bogus positive byte count. + emitter.label_global("__rt_sys_sendto"); + emitter.instruction("sub rsp, 136"); // shadow space, stack args, sockaddr, and emulation locals + emitter.instruction("mov QWORD PTR [rsp + 112], rdi"); // preserve socket + emitter.instruction("mov QWORD PTR [rsp + 120], rsi"); // preserve buffer + emitter.instruction("mov QWORD PTR [rsp + 48], rdx"); // preserve byte count + emitter.instruction("mov QWORD PTR [rsp + 56], r10"); // preserve flags + emitter.instruction("mov QWORD PTR [rsp + 88], r8"); // preserve destination address + emitter.instruction("mov QWORD PTR [rsp + 96], r9"); // preserve destination length + emitter.instruction("test r8, r8"); // destination supplied? + emitter.instruction("jz .Lsendto_address_ready"); // connected socket uses no explicit address + emitter.instruction("cmp WORD PTR [r8], 1"); // AF_UNIX destination? + emitter.instruction("jne .Lsendto_address_ready"); // native sockaddr passes through + emitter.instruction("mov rdi, QWORD PTR [rsp + 112]"); // sending socket + emitter.instruction("call __rt_win_unix_find_socket"); // detect loopback fallback metadata + emitter.instruction("test rax, rax"); // emulated AF_UNIX sender? + emitter.instruction("jz .Lsendto_address_ready"); // native AF_UNIX resolves paths across processes + emitter.instruction("mov QWORD PTR [rsp + 128], rax"); // retain fallback sender metadata + emitter.instruction("mov r10, QWORD PTR [rsp + 88]"); // restore destination sockaddr_un + emitter.instruction("lea rdi, [r10 + 2]"); // requested Unix path + emitter.instruction("call __rt_win_unix_find_path"); // resolve endpoint registry + emitter.instruction("test rax, rax"); // endpoint exists? + emitter.instruction("jz .Lsendto_path_missing"); // deterministic ENOENT + emitter.instruction("mov QWORD PTR [rsp + 104], rax"); // retain endpoint record + emitter.instruction("mov r10, QWORD PTR [rsp + 104]"); // destination endpoint + emitter.instruction("mov rax, QWORD PTR [rsp + 128]"); // fallback sender metadata + emitter.instruction("mov r11, QWORD PTR [rax + 8]"); // sender socket type + emitter.instruction("cmp r11, QWORD PTR [r10 + 16]"); // compatible endpoint type? + emitter.instruction("jne .Lsendto_type_mismatch"); // deterministic EPROTOTYPE + emitter.instruction("pxor xmm0, xmm0"); // zero native sockaddr_in + emitter.instruction("movdqu XMMWORD PTR [rsp + 64], xmm0"); // clear 16-byte address + emitter.instruction("mov WORD PTR [rsp + 64], 2"); // Winsock AF_INET + emitter.instruction("mov r11w, WORD PTR [r10 + 24]"); // registered network-order port + emitter.instruction("mov WORD PTR [rsp + 66], r11w"); // sockaddr_in port + emitter.instruction("mov DWORD PTR [rsp + 68], 0x0100007f"); // 127.0.0.1 in network byte order + emitter.instruction("lea rax, [rsp + 64]"); // translated destination address + emitter.instruction("mov QWORD PTR [rsp + 88], rax"); // use translated sockaddr + emitter.instruction("mov QWORD PTR [rsp + 96], 16"); // native sockaddr_in length + emitter.label(".Lsendto_address_ready"); + emitter.instruction("mov rax, QWORD PTR [rsp + 88]"); // destination address + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // destination → 5th MSx64 arg + emitter.instruction("mov rax, QWORD PTR [rsp + 96]"); // destination length + emitter.instruction("mov QWORD PTR [rsp + 40], rax"); // addrlen → 6th MSx64 arg + emitter.instruction("mov r9, QWORD PTR [rsp + 56]"); // flags → r9 + emitter.instruction("mov r8, QWORD PTR [rsp + 48]"); // len → r8 + emitter.instruction("mov rdx, QWORD PTR [rsp + 120]"); // buffer → rdx + emitter.instruction("mov rcx, QWORD PTR [rsp + 112]"); // socket → rcx + emitter.instruction("call sendto"); // sendto(socket, buf, len, flags, dest_addr, addrlen) + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je .Lsendto_capture_errno"); // publish Winsock failure + emitter.instruction("cdqe"); // sign-extend eax → rax (SOCKET_ERROR=-1 negative) + emitter.instruction("add rsp, 136"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lsendto_path_missing"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 2"); // ENOENT + emitter.instruction("jmp .Lsendto_local_failure"); // return failure + emitter.label(".Lsendto_type_mismatch"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 91"); // EPROTOTYPE + emitter.label(".Lsendto_local_failure"); + emitter.instruction("mov rax, -1"); // SOCKET_ERROR + emitter.instruction("add rsp, 136"); // restore stack + emitter.instruction("ret"); // return failure + emitter.label(".Lsendto_capture_errno"); + emitter.instruction("call __rt_wsa_capture_errno"); // capture WSAGetLastError and return -1 + emitter.instruction("add rsp, 136"); // restore stack + emitter.instruction("ret"); // return SOCKET_ERROR + emitter.blank(); + + // recvfrom: 6 args — socket, buf, len, flags, src_addr, &addrlen + // SysV: rdi=socket, rsi=buf, rdx=len, r10=flags, r8=src_addr, r9=&addrlen + // MSx64: rcx, rdx, r8, r9, [rsp+32], [rsp+40] + // Winsock recvfrom returns the byte count (>= 0) or SOCKET_ERROR (-1) as a 32-bit int; + // stream_socket_recvfrom.rs:204 sign-tests it (`cmp rax,0; jl`), so cdqe sign-extends a + // -1 failure into a 64-bit negative instead of a bogus positive byte count. On failure, + // also translates WSAGetLastError() to a POSIX errno and stores it into __rt_errno (see + // __rt_win32_errno_from_code, shims_c_symbols.rs) — this is what the fgets/fread EAGAIN + // check needs to distinguish a nonblocking would-block from a real error/EOF. + emitter.label_global("__rt_sys_recvfrom"); + emitter.instruction("sub rsp, 152"); // shadow space, stack args, sockaddr, and emulation locals + emitter.instruction("mov QWORD PTR [rsp + 112], rdi"); // preserve socket + emitter.instruction("mov QWORD PTR [rsp + 120], rsi"); // preserve buffer + emitter.instruction("mov QWORD PTR [rsp + 48], rdx"); // preserve byte count + emitter.instruction("mov QWORD PTR [rsp + 56], r10"); // preserve flags + emitter.instruction("mov QWORD PTR [rsp + 88], r8"); // preserve caller source sockaddr + emitter.instruction("mov QWORD PTR [rsp + 96], r9"); // preserve caller source length pointer + emitter.instruction("mov QWORD PTR [rsp + 136], r8"); // retain caller sockaddr across redirection + emitter.instruction("mov QWORD PTR [rsp + 144], r9"); // retain caller length pointer across redirection + emitter.instruction("mov QWORD PTR [rsp + 104], 0"); // native address output by default + emitter.instruction("test r8, r8"); // caller requested source address? + emitter.instruction("jz .Lrecvfrom_address_ready"); // no address translation needed + emitter.instruction("call __rt_win_unix_find_socket"); // detect emulated Unix socket + emitter.instruction("test rax, rax"); // emulated socket metadata? + emitter.instruction("jz .Lrecvfrom_address_ready"); // ordinary socket writes caller buffer + emitter.instruction("pxor xmm0, xmm0"); // zero native sockaddr_in + emitter.instruction("movdqu XMMWORD PTR [rsp + 64], xmm0"); // clear 16-byte address buffer + emitter.instruction("mov DWORD PTR [rsp + 80], 16"); // native address capacity + emitter.instruction("lea rax, [rsp + 64]"); // temporary native source address + emitter.instruction("mov QWORD PTR [rsp + 88], rax"); // redirect Winsock output + emitter.instruction("lea rax, [rsp + 80]"); // temporary native source length + emitter.instruction("mov QWORD PTR [rsp + 96], rax"); // redirect Winsock length output + emitter.instruction("mov QWORD PTR [rsp + 104], 1"); // synthesize AF_UNIX source afterward + emitter.label(".Lrecvfrom_address_ready"); + emitter.instruction("mov rax, QWORD PTR [rsp + 88]"); // native source address + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // source address → 5th MSx64 arg + emitter.instruction("mov rax, QWORD PTR [rsp + 96]"); // native source length pointer + emitter.instruction("mov QWORD PTR [rsp + 40], rax"); // source length → 6th MSx64 arg + emitter.instruction("mov r9, QWORD PTR [rsp + 56]"); // flags → r9 + emitter.instruction("mov r8, QWORD PTR [rsp + 48]"); // len → r8 + emitter.instruction("mov rdx, QWORD PTR [rsp + 120]"); // buffer → rdx + emitter.instruction("mov rcx, QWORD PTR [rsp + 112]"); // socket → rcx + emitter.instruction("call recvfrom"); // recvfrom(socket, buf, len, flags, src_addr, &addrlen) + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je .Lrecvfrom_fail"); // failure: translate WSAGetLastError and return -1 + emitter.instruction("cdqe"); // sign-extend byte count + emitter.instruction("mov QWORD PTR [rsp + 128], rax"); // preserve result across address synthesis + emitter.instruction("cmp QWORD PTR [rsp + 104], 0"); // emulated Unix source requested? + emitter.instruction("je .Lrecvfrom_success"); // native output is already complete + emitter.instruction("movzx edi, WORD PTR [rsp + 66]"); // sender's network-order loopback port + emitter.instruction("call __rt_win_unix_find_port"); // resolve sender endpoint name + emitter.instruction("mov rdi, rax"); // endpoint or NULL for anonymous sender + emitter.instruction("mov rsi, QWORD PTR [rsp + 136]"); // caller sockaddr buffer + emitter.instruction("mov rdx, QWORD PTR [rsp + 144]"); // caller length pointer + emitter.instruction("call __rt_win_unix_write_sockaddr"); // synthesize AF_UNIX source name + emitter.label(".Lrecvfrom_success"); + emitter.instruction("mov rax, QWORD PTR [rsp + 128]"); // restore byte count + emitter.instruction("add rsp, 152"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lrecvfrom_fail"); + emitter.instruction("call __rt_wsa_capture_errno"); // capture WSAGetLastError and return -1 + emitter.instruction("add rsp, 152"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits the `__rt_winsock_init` helper that calls `WSAStartup(MAKEWORD(2,2), &wsadata)`. +/// +/// Allocates a 400-byte `WSADATA` buffer on the stack, loads `MAKEWORD(2,2)` (0x0202) +/// into the SysV first arg (`edi`), zero-inits the WSADATA buffer, shuffles to MSx64 +/// (`rcx`=version, `rdx`=&wsadata), and calls `WSAStartup`. The return value is ignored +/// (Winsock init is best-effort; socket calls will fail with a meaningful WSAGetLastError +/// if startup failed). Called from the Windows `main` wrapper before `__elephc_main`. +pub(super) fn emit_winsock_init(emitter: &mut Emitter) { + emitter.label_global("__rt_winsock_init"); + // -- stack frame: shadow(32) + WSADATA(400) + version spill(8) + pad to 16-byte align -- + // 32 + 400 + 8 = 440; round up to 456 (456 ≡ 8 mod 16, re-aligning the entry rsp ≡ 8). + emitter.instruction("sub rsp, 456"); // shadow(32) + WSADATA(400) + spill(8) + pad(16), 16-byte aligned + // -- zero the 400-byte WSADATA buffer at [rsp+32..432) -- + emitter.instruction("cld"); // forward direction for rep stosb + emitter.instruction("lea rdi, [rsp + 32]"); // dest = WSADATA buffer start (above shadow space) + emitter.instruction("xor eax, eax"); // zero fill byte + emitter.instruction("mov ecx, 400"); // WSADATA size in bytes (Microsoft's MAXGETHOSTSTRUCT-equivalent for v2.2) + emitter.instruction("rep stosb"); // zero the whole WSADATA buffer so unused fields are determinate + // -- WSAStartup(MAKEWORD(2,2), &wsadata) -- + emitter.instruction("mov edi, 0x0202"); // MAKEWORD(2,2) = 0x0202 (minor=2, major=2) — SysV arg1 + emitter.instruction("lea rsi, [rsp + 32]"); // &wsadata — SysV arg2 + emitter.instruction("mov rcx, rdi"); // MSx64 arg1 = version (MAKEWORD(2,2)) + emitter.instruction("mov rdx, rsi"); // MSx64 arg2 = &wsadata + emitter.instruction("call WSAStartup"); // initialize Winsock 2.2 (return in eax: 0 = success, ignored) + emitter.instruction("add rsp, 456"); // restore stack + emitter.instruction("ret"); // return to caller + emitter.blank(); +} + +/// Emits the `__rt_winsock_cleanup` helper that calls `WSACleanup()`. +/// +/// Releases Winsock resources. Safe to call even when `WSAStartup` was never invoked +/// (Winsock returns an error in that case, which we ignore). Called from `__rt_sys_exit` +/// before `ExitProcess` so socket resources are released on process termination. +pub(super) fn emit_winsock_cleanup(emitter: &mut Emitter) { + emitter.label_global("__rt_winsock_cleanup"); + emitter.instruction("sub rsp, 40"); // shadow space (16-byte aligned: entry ≡ 8, 40 ≡ 8 → 0) + emitter.instruction("call WSACleanup"); // release Winsock resources (return ignored) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return to caller + emitter.blank(); +} + +/// Emits an accept4 shim (maps to accept on Windows). +pub(super) fn emit_shim_accept4(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_accept4"); + emitter.instruction("sub rsp, 8"); // align internal call + emitter.instruction("call __rt_sys_accept"); // accept and preserve Unix-emulation metadata + emitter.instruction("add rsp, 8"); // restore stack + emitter.instruction("ret"); // return accepted socket or failure + emitter.blank(); +} + +/// Emits setsockopt shim — 5 args: socket, level, optname, optval, optlen. +/// +/// SysV: rdi=socket, rsi=level, rdx=optname, r10=optval, r8=optlen +/// MSx64: rcx, rdx, r8, r9, [rsp+32] +/// +/// Winsock setsockopt returns a 32-bit `int` status (0 success, `SOCKET_ERROR` = -1 on +/// failure); `stream_set_timeout.rs:75` sign-tests it (`cmp rax,0; jl`), so `cdqe` +/// sign-extends a -1 failure into a 64-bit negative instead of a false-positive success. +/// +/// F5 fix (Winsock sockopt-value translation): elephc's x86_64 socket-option emitters +/// (`stream_set_timeout.rs`, `apply_socket_opts.rs`, `stream_socket_server_v6.rs`) stage +/// raw POSIX/Linux `level`/`optname` numbers before the `syscall(54)` that +/// `windows_transform.rs` rewrites into `call __rt_sys_setsockopt`; those numbers do NOT +/// match Winsock's, so forwarding them unmodified silently sets the wrong (or an invalid) +/// option. This shim now translates the level+optname pairs elephc actually emits before +/// calling Winsock `setsockopt`: +/// - level: POSIX `SOL_SOCKET`(1) → Winsock `0xFFFF`. Any other level (e.g. POSIX +/// `IPPROTO_TCP`=6, used by `TCP_NODELAY`, or `IPPROTO_IPV6`=41) is already numerically +/// identical on Winsock, so it is left unchanged. +/// - optname (only when the ORIGINAL level was `SOL_SOCKET`): POSIX `SO_RCVTIMEO`(20) → +/// `0x1006`, `SO_SNDTIMEO`(21) → `0x1005`, `SO_REUSEADDR`(2) → `4`, `SO_KEEPALIVE`(9) → +/// `8`, `SO_BROADCAST`(6) → `0x20`, `SO_REUSEPORT`(15) → `4`. `SO_KEEPALIVE`/ +/// `SO_SNDTIMEO` have no elephc consumer today but are translated anyway per the +/// audited mapping table, since a future consumer would otherwise silently inherit the +/// bug. `SO_REUSEPORT` has no Winsock equivalent (`WSAENOPROTOOPT` if forwarded raw), so +/// it is mapped to Winsock `SO_REUSEADDR`(4) — the exact substitution php-src uses on +/// Windows for equivalent address-reuse; double-setting `SO_REUSEADDR` (when elephc also +/// emits optname 2) is idempotent/harmless. Any other `SOL_SOCKET` optname passes +/// through UNCHANGED rather than being silently dropped. +/// - optname (only when the ORIGINAL level was `IPPROTO_IPV6`=41, itself identical on +/// Winsock): POSIX `IPV6_V6ONLY`(26) → Winsock `27`; the payload is a plain 4-byte int +/// (no conversion). Any other `IPPROTO_IPV6` optname passes through UNCHANGED. +/// - payload for `SO_RCVTIMEO`/`SO_SNDTIMEO` ONLY: php-src's Windows trap — Winsock takes +/// a plain `DWORD` millisecond count for these two options, not a `struct timeval`. The +/// shim reads the POSIX `timeval` elephc already built at `[r10]` (`tv_sec`@+0, +/// `tv_usec`@+8, 8 bytes each), computes `ms = tv_sec*1000 + tv_usec/1000` into a stack +/// DWORD (in the frame's already-reserved-but-unused `[rsp+40..56)` alignment padding), +/// and redirects `optval`/`optlen` to that DWORD (`optlen=4`) before the Winsock call. +/// Every other option's payload (a 4-byte `int`) is already binary-compatible and is +/// forwarded unchanged. +/// +/// F5 follow-up fix (rdx clobber in the timeout payload): the ms-conversion above divides +/// `tv_usec` by 1000 via `cqo`/`idiv`, and both destructively overwrite `rdx` (`cqo` +/// sign-extends `rax` into `rdx:rax`; `idiv` then leaves the remainder in `rdx`) — but +/// `rdx` is exactly where the translated Winsock optname (`0x1006`/`0x1005`) was staged +/// just before the jump into this block. Left alone, the optname is silently replaced by +/// `tv_usec % 1000` and Winsock receives garbage. The shim now stashes `rdx` to the +/// unused `[rsp+40]` pad slot before `cqo` and restores it immediately after `idiv`, so +/// the optname survives to `.Lsetsockopt_after_optname`'s `mov r8, rdx`. +pub(super) fn emit_shim_setsockopt(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_setsockopt"); + emitter.instruction("sub rsp, 56"); // shadow(32) + stack arg(8) + alignment(16) + // -- translate POSIX level/optname to Winsock values (SOL_SOCKET + IPPROTO_IPV6) -- + emitter.instruction("cmp rsi, 1"); // level == POSIX SOL_SOCKET? + emitter.instruction("je .Lsetsockopt_sol_socket"); // -> SOL_SOCKET optname table + emitter.instruction("cmp rsi, 41"); // level == POSIX IPPROTO_IPV6? + emitter.instruction("je .Lsetsockopt_ipv6"); // -> IPPROTO_IPV6 optname table + emitter.instruction("jmp .Lsetsockopt_after_optname"); // other levels (e.g. IPPROTO_TCP=6) need no translation + emitter.label(".Lsetsockopt_sol_socket"); + emitter.instruction("cmp rdx, 20"); // optname == POSIX SO_RCVTIMEO? + emitter.instruction("je .Lsetsockopt_is_rcvtimeo"); // -> Winsock SO_RCVTIMEO + ms payload + emitter.instruction("cmp rdx, 21"); // optname == POSIX SO_SNDTIMEO? + emitter.instruction("je .Lsetsockopt_is_sndtimeo"); // -> Winsock SO_SNDTIMEO + ms payload + emitter.instruction("cmp rdx, 2"); // optname == POSIX SO_REUSEADDR? + emitter.instruction("je .Lsetsockopt_is_reuseaddr"); // -> Winsock SO_REUSEADDR + emitter.instruction("cmp rdx, 9"); // optname == POSIX SO_KEEPALIVE? + emitter.instruction("je .Lsetsockopt_is_keepalive"); // -> Winsock SO_KEEPALIVE + emitter.instruction("cmp rdx, 6"); // optname == POSIX SO_BROADCAST? + emitter.instruction("je .Lsetsockopt_is_broadcast"); // -> Winsock SO_BROADCAST + emitter.instruction("cmp rdx, 15"); // optname == POSIX SO_REUSEPORT? + emitter.instruction("je .Lsetsockopt_is_reuseport"); // -> Winsock SO_REUSEADDR (no SO_REUSEPORT on Win) + emitter.instruction("jmp .Lsetsockopt_after_optname"); // unknown SOL_SOCKET optname: pass through unchanged + emitter.label(".Lsetsockopt_is_rcvtimeo"); + emitter.instruction("mov rdx, 0x1006"); // Winsock SO_RCVTIMEO + emitter.instruction("jmp .Lsetsockopt_timeout_payload"); // apply the shared ms-payload conversion + emitter.label(".Lsetsockopt_is_sndtimeo"); + emitter.instruction("mov rdx, 0x1005"); // Winsock SO_SNDTIMEO + emitter.instruction("jmp .Lsetsockopt_timeout_payload"); // apply the shared ms-payload conversion + emitter.label(".Lsetsockopt_is_reuseaddr"); + emitter.instruction("mov rdx, 4"); // Winsock SO_REUSEADDR + emitter.instruction("jmp .Lsetsockopt_after_optname"); // payload already a 4-byte int: no conversion + emitter.label(".Lsetsockopt_is_keepalive"); + emitter.instruction("mov rdx, 8"); // Winsock SO_KEEPALIVE + emitter.instruction("jmp .Lsetsockopt_after_optname"); // payload already a 4-byte int: no conversion + emitter.label(".Lsetsockopt_is_broadcast"); + emitter.instruction("mov rdx, 0x20"); // Winsock SO_BROADCAST + emitter.instruction("jmp .Lsetsockopt_after_optname"); // payload already a 4-byte int: no conversion + emitter.label(".Lsetsockopt_is_reuseport"); + emitter.instruction("mov rdx, 4"); // Winsock SO_REUSEADDR: php-src maps SO_REUSEPORT here on Windows + emitter.instruction("jmp .Lsetsockopt_after_optname"); // payload already a 4-byte int: no conversion + emitter.label(".Lsetsockopt_ipv6"); + emitter.instruction("cmp rdx, 26"); // optname == POSIX IPV6_V6ONLY? + emitter.instruction("je .Lsetsockopt_is_v6only"); // -> Winsock IPV6_V6ONLY (27) + emitter.instruction("jmp .Lsetsockopt_after_optname"); // other IPPROTO_IPV6 optnames pass through unchanged + emitter.label(".Lsetsockopt_is_v6only"); + emitter.instruction("mov rdx, 27"); // Winsock IPV6_V6ONLY (Linux 26 -> Windows 27) + emitter.instruction("jmp .Lsetsockopt_after_optname"); // payload already a 4-byte int: no conversion + // -- SO_RCVTIMEO/SO_SNDTIMEO payload: Windows wants a DWORD millisecond -- + // -- count, not a `struct timeval`; convert the POSIX timeval at [r10] -- + emitter.label(".Lsetsockopt_timeout_payload"); + emitter.instruction("mov rax, QWORD PTR [r10 + 8]"); // tv_usec + emitter.instruction("mov QWORD PTR [rsp + 40], rdx"); // stash Winsock optname: cqo/idiv below clobber rdx + emitter.instruction("cqo"); // sign-extend rax into rdx:rax for idiv + emitter.instruction("mov r11, 1000"); // divisor: microseconds per millisecond + emitter.instruction("idiv r11"); // rax = tv_usec / 1000 (rdx = remainder, discarded) + emitter.instruction("mov rdx, QWORD PTR [rsp + 40]"); // restore optname clobbered by cqo/idiv + emitter.instruction("mov r11, rax"); // stash the usec-derived ms + emitter.instruction("mov rax, QWORD PTR [r10]"); // tv_sec + emitter.instruction("imul rax, rax, 1000"); // tv_sec * 1000 + emitter.instruction("add rax, r11"); // ms = tv_sec*1000 + tv_usec/1000 + emitter.instruction("mov DWORD PTR [rsp + 48], eax"); // stash ms DWORD in unused frame padding + emitter.instruction("lea r10, [rsp + 48]"); // optval now points at the ms DWORD + emitter.instruction("mov r8, 4"); // optlen = sizeof(DWORD) + emitter.label(".Lsetsockopt_after_optname"); + emitter.instruction("cmp rsi, 1"); // level == POSIX SOL_SOCKET? + emitter.instruction("jne .Lsetsockopt_after_level"); // other levels pass through unchanged + emitter.instruction("mov rsi, 0xffff"); // Winsock SOL_SOCKET + emitter.label(".Lsetsockopt_after_level"); + emitter.instruction("mov QWORD PTR [rsp + 32], r8"); // optlen → 5th arg (stack) + emitter.instruction("mov r9, r10"); // optval → r9 (4th arg) + emitter.instruction("mov r8, rdx"); // optname → r8 (3rd arg) + emitter.instruction("mov rdx, rsi"); // level → rdx (2nd arg) + emitter.instruction("mov rcx, rdi"); // socket → rcx (1st arg) + emitter.instruction("call setsockopt"); // setsockopt(socket, level, optname, optval, optlen) + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je .Lsetsockopt_capture_errno"); // publish Winsock option failure + emitter.instruction("cdqe"); // sign-extend eax → rax (SOCKET_ERROR=-1 negative) + emitter.instruction("add rsp, 56"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lsetsockopt_capture_errno"); + emitter.instruction("call __rt_wsa_capture_errno"); // capture WSAGetLastError and return -1 + emitter.instruction("add rsp, 56"); // restore stack + emitter.instruction("ret"); // return SOCKET_ERROR + emitter.blank(); +} + +/// Emits getsockopt shim — 5 args: socket, level, optname, optval, &optlen. +/// +/// SysV: rdi=socket, rsi=level, rdx=optname, r10=optval, r8=&optlen +/// MSx64: rcx, rdx, r8, r9, [rsp+32] +/// +/// INVESTIGATED (sign-extension audit): Winsock getsockopt has the same 32-bit int-status +/// shape as setsockopt (0 success, `SOCKET_ERROR` = -1 on failure), but as of this audit +/// there is NO consumer anywhere in the codebase — no PHP builtin lowers to syscall 55 +/// (`grep -rn "getsockopt\|socket_get_option" src/` outside this file and the +/// windows_transform.rs syscall-number table returns nothing). The sign-extension triplet +/// (int-status return ∧ a consumer sign-tests it ∧ cdqe absent) fails on the second +/// conjunct: there is no consumer to sign-test it, so `cdqe` is deliberately NOT added +/// here. If a `socket_get_option`/`getsockopt` consumer is ever wired up on the syscall-55 +/// path, apply the same `cdqe` fix as `emit_shim_setsockopt` above at that time. +/// +/// F5 audit (Winsock sockopt-value translation): `emit_shim_setsockopt` above now +/// translates POSIX `level`/`optname`/timeout-payload values to their Winsock +/// equivalents (elephc staged raw POSIX numbers, which do not match Winsock's). This +/// getsockopt shim is DELIBERATELY left untranslated: it is latent (same "no consumer" +/// finding as the sign-extension audit above), and translating a path nothing calls +/// would be speculative/unverifiable. If a `socket_get_option`/`getsockopt` consumer is +/// ever wired up, apply the same level/optname/payload translation as +/// `emit_shim_setsockopt` at that time — do not assume this shim is Winsock-correct +/// until then. +pub(super) fn emit_shim_getsockopt(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_getsockopt"); + emitter.instruction("sub rsp, 56"); // shadow(32) + stack arg(8) + alignment(16) + emitter.instruction("mov QWORD PTR [rsp + 32], r8"); // &optlen → 5th arg (stack) + emitter.instruction("mov r9, r10"); // optval → r9 (4th arg) + emitter.instruction("mov r8, rdx"); // optname → r8 (3rd arg) + emitter.instruction("mov rdx, rsi"); // level → rdx (2nd arg) + emitter.instruction("mov rcx, rdi"); // socket → rcx (1st arg) + emitter.instruction("call getsockopt"); // getsockopt(socket, level, optname, optval, &optlen) + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je .Lgetsockopt_capture_errno"); // publish Winsock option failure + emitter.instruction("cdqe"); // sign-extend 32-bit status + emitter.instruction("add rsp, 56"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lgetsockopt_capture_errno"); + emitter.instruction("call __rt_wsa_capture_errno"); // capture WSAGetLastError and return -1 + emitter.instruction("add rsp, 56"); // restore stack + emitter.instruction("ret"); // return SOCKET_ERROR + emitter.blank(); +} + +/// Emits the PHP-compatible Windows `socketpair` loopback implementation. +/// +/// php-src accepts only `AF_INET` on Windows and emulates the pair with a +/// loopback listener, client connection, and accepted server socket. The +/// runtime preserves both opaque 64-bit `SOCKET` values in the caller's +/// output pair. +pub(super) fn emit_shim_socketpair(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_socketpair"); + emitter.instruction("sub rsp, 104"); // shadow space, sockaddr_in, handles, output, error locals + emitter.instruction("mov QWORD PTR [rsp + 80], r10"); // preserve int sv[2] output pointer + emitter.instruction("mov DWORD PTR [rsp + 92], edx"); // preserve caller protocol across Winsock calls + emitter.instruction("cmp edi, 2"); // PHP_WIN32 socketpair accepts AF_INET only + emitter.instruction("jne .Lsocketpair_domain_fail"); // reject AF_UNIX and every non-IPv4 family + emitter.instruction("mov rax, rsi"); // socket type + emitter.instruction("and eax, 0xf"); // isolate SOCK_TYPE_MASK + emitter.instruction("cmp eax, 1"); // SOCK_STREAM? + emitter.instruction("jne .Lsocketpair_domain_fail"); // only stream pairs are emulated + emitter.instruction("mov QWORD PTR [rsp + 56], -1"); // listener = INVALID_SOCKET + emitter.instruction("mov QWORD PTR [rsp + 64], -1"); // client = INVALID_SOCKET + emitter.instruction("mov QWORD PTR [rsp + 72], -1"); // accepted = INVALID_SOCKET + emitter.instruction("mov WORD PTR [rsp + 32], 2"); // sockaddr_in.sin_family = AF_INET + emitter.instruction("mov WORD PTR [rsp + 34], 0"); // ephemeral port + emitter.instruction("mov DWORD PTR [rsp + 36], 0x0100007f"); // sin_addr = htonl(INADDR_LOOPBACK) + emitter.instruction("mov QWORD PTR [rsp + 40], 0"); // zero sin_zero[8] + emitter.instruction("mov DWORD PTR [rsp + 48], 16"); // sockaddr length + emitter.instruction("mov ecx, 2"); // AF_INET + emitter.instruction("mov edx, 1"); // SOCK_STREAM + emitter.instruction("mov r8d, DWORD PTR [rsp + 92]"); // caller-selected protocol + emitter.instruction("call socket"); // create loopback listener + emitter.instruction("cmp rax, -1"); // INVALID_SOCKET? + emitter.instruction("je .Lsocketpair_native_fail"); // capture creation failure + emitter.instruction("mov QWORD PTR [rsp + 56], rax"); // retain listener + emitter.instruction("mov rcx, rax"); // listener socket + emitter.instruction("lea rdx, [rsp + 32]"); // loopback sockaddr + emitter.instruction("mov r8d, 16"); // sockaddr length + emitter.instruction("call bind"); // bind ephemeral loopback port + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je .Lsocketpair_native_fail"); // cleanup listener + emitter.instruction("mov rcx, QWORD PTR [rsp + 56]"); // listener + emitter.instruction("mov edx, 1"); // backlog one + emitter.instruction("call listen"); // begin accepting + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je .Lsocketpair_native_fail"); // cleanup listener + emitter.instruction("mov rcx, QWORD PTR [rsp + 56]"); // listener + emitter.instruction("lea rdx, [rsp + 32]"); // receive bound address + emitter.instruction("lea r8, [rsp + 48]"); // socklen pointer + emitter.instruction("call getsockname"); // discover ephemeral port + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je .Lsocketpair_native_fail"); // cleanup listener + emitter.instruction("mov ecx, 2"); // AF_INET + emitter.instruction("mov edx, 1"); // SOCK_STREAM + emitter.instruction("mov r8d, DWORD PTR [rsp + 92]"); // caller-selected protocol + emitter.instruction("call socket"); // create client endpoint + emitter.instruction("cmp rax, -1"); // INVALID_SOCKET? + emitter.instruction("je .Lsocketpair_native_fail"); // cleanup listener + emitter.instruction("mov QWORD PTR [rsp + 64], rax"); // retain client + emitter.instruction("mov rcx, rax"); // client socket + emitter.instruction("lea rdx, [rsp + 32]"); // listener loopback address + emitter.instruction("mov r8d, 16"); // sockaddr length + emitter.instruction("call connect"); // connect client to listener + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je .Lsocketpair_native_fail"); // cleanup both sockets + emitter.instruction("mov rcx, QWORD PTR [rsp + 56]"); // listener socket + emitter.instruction("xor edx, edx"); // no peer address requested + emitter.instruction("xor r8d, r8d"); // no peer address length + emitter.instruction("call accept"); // accept connected server endpoint + emitter.instruction("cmp rax, -1"); // INVALID_SOCKET? + emitter.instruction("je .Lsocketpair_native_fail"); // cleanup client/listener + emitter.instruction("mov QWORD PTR [rsp + 72], rax"); // retain accepted endpoint + emitter.instruction("mov rcx, QWORD PTR [rsp + 56]"); // listener no longer needed + emitter.instruction("call closesocket"); // close listener + emitter.instruction("mov r10, QWORD PTR [rsp + 80]"); // int sv[2] output + emitter.instruction("mov rax, QWORD PTR [rsp + 64]"); // full-width client SOCKET + emitter.instruction("mov QWORD PTR [r10], rax"); // sv[0] = client + emitter.instruction("mov rax, QWORD PTR [rsp + 72]"); // full-width accepted SOCKET + emitter.instruction("mov QWORD PTR [r10 + 8], rax"); // sv[1] = server + emitter.instruction("xor eax, eax"); // POSIX success + emitter.instruction("add rsp, 104"); // restore stack + emitter.instruction("ret"); // return 0 + emitter.label(".Lsocketpair_native_fail"); + emitter.instruction("call WSAGetLastError"); // capture failure before cleanup calls + emitter.instruction("mov DWORD PTR [rsp + 88], eax"); // preserve native error + for (offset, skip) in [(72, "server"), (64, "client"), (56, "listener")] { + emitter.instruction(&format!("mov rcx, QWORD PTR [rsp + {offset}]")); // socket candidate + emitter.instruction("cmp rcx, -1"); // allocated endpoint? + emitter.instruction(&format!("je .Lsocketpair_skip_{skip}")); // skip invalid socket + emitter.instruction("call closesocket"); // release endpoint + emitter.label(&format!(".Lsocketpair_skip_{skip}")); + } + emitter.instruction("mov eax, DWORD PTR [rsp + 88]"); // restore WSA error + emitter.instruction("mov DWORD PTR [rip + __rt_wsa_last_error], eax"); // retain native Winsock state + emitter.instruction("call __rt_win32_errno_from_code"); // translate POSIX errno + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish errno + emitter.instruction("mov rax, -1"); // POSIX failure + emitter.instruction("add rsp, 104"); // restore stack + emitter.instruction("ret"); // return -1 + emitter.label(".Lsocketpair_domain_fail"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 92"); // ENOPROTOOPT, matching socketpair_win32 + emitter.instruction("mov rax, -1"); // reject unsupported pair family/type + emitter.instruction("add rsp, 104"); // restore stack + emitter.instruction("ret"); // return -1 + emitter.blank(); +} + +/// Emits the `__rt_sys_pselect6` shim: converts the Linux `pselect6` syscall +/// (syscall 270) to ws2_32 `select`. elephc's `__rt_stream_select` x86_64 path +/// builds Linux fd_set bitmaps (one qword per set, nfds=64) and emits +/// `syscall(270)` with SysV args, which the Windows syscall transform routes +/// here as a normal `call`. +/// +/// SysV args received (passed in SysV registers by the transform): +/// - `edi` = nfds (int; elephc always passes 64 — the bitmap is one qword) +/// - `rsi` = readfds (Linux fd_set* bitmap; NULL allowed) +/// - `rdx` = writefds (Linux fd_set* bitmap; NULL allowed) +/// - `r10` = exceptfds (Linux fd_set* bitmap; NULL allowed) +/// - `r8` = timeout (struct timespec* : sec@0 i64, nsec@8 i64; NULL = block) +/// - `r9` = sigmask (NULL — ignored entirely) +/// +/// Conversion: +/// - Each non-NULL Linux fd_set bitmap (qword) is converted into a Windows +/// `fd_set` (winsock2, 520 bytes: `u_int fd_count` @0, 4-byte pad, `SOCKET +/// fd_array[64]` @8). For each set bit `b` (0..nfds-1), `b` is appended to +/// `fd_array` and `fd_count` incremented. NULL sets pass NULL to `select`. +/// - The Linux `struct timespec` (sec i64, nsec i64) is converted to the +/// Windows `struct timeval` (tv_sec i32 @0, tv_usec i32 @4): tv_sec = sec +/// (low 32 bits), tv_usec = nsec / 1000. A NULL timeout passes NULL (block). +/// - After `select` returns: on SOCKET_ERROR (-1), the three Linux bitmaps are +/// zeroed (only the non-NULL ones) and -1 is returned. On success, each +/// non-NULL Linux bitmap is zeroed and rebuilt from the post-select Windows +/// `fd_set` (which `select` rewrites in place to contain only ready +/// descriptors): for each fd in `fd_array[0..fd_count)`, bit `fd` is set in +/// the Linux bitmap qword (`or [linux_fds], 1 << fd`). +/// +/// `select` returns its ready count/`SOCKET_ERROR` as a 32-bit `int` in `eax`. +/// The shim sign-extends that result before storing it and compares `eax` with +/// the 32-bit `SOCKET_ERROR` sentinel, so failures enter the errno/bitmap reset +/// path while successful counts remain suitable for the runtime's 64-bit tests. +/// +/// Frame: 1688 bytes (`sub rsp, 1688`; 1688 ≡ 8 mod 16, so rsp ≡ 0 at the +/// `call select` since the shim is entered via `call` with rsp ≡ 8). Layout: +/// - [rsp+0..32) shadow space (MSx64) +/// - [rsp+32] nfds spill (edi, zero-extended to 64 bits) +/// - [rsp+40] readfds ptr (rsi) +/// - [rsp+48] writefds ptr (rdx) +/// - [rsp+56] exceptfds ptr (r10) +/// - [rsp+64] timeout ptr (r8) +/// - [rsp+72] saved select result +/// - [rsp+80] win_read ptr (select arg2) +/// - [rsp+88] win_write ptr (select arg3) +/// - [rsp+96] win_except ptr (select arg4) +/// - [rsp+104] win_timeval ptr (select arg5) +/// - [rsp+112] win_read fd_set (520 bytes) +/// - [rsp+632] win_write fd_set (520 bytes) +/// - [rsp+1152] win_except fd_set (520 bytes) +/// - [rsp+1672] win_timeval (8 bytes: tv_sec i32 @0, tv_usec i32 @4) +/// - [rsp+1680] padding (8 bytes) +pub(super) fn emit_shim_pselect6(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_pselect6"); + // -- frame: 1688 bytes; 1688 ≡ 8 mod 16 keeps rsp ≡ 0 at the ws2_32 select call -- + emitter.instruction("sub rsp, 1688"); // allocate frame (1688 ≡ 8 mod 16) + // -- spill incoming SysV args (volatile across fd_set build and select call) -- + emitter.instruction("mov eax, edi"); // zero-extend nfds (edi, 32-bit) into rax + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // spill nfds + emitter.instruction("mov QWORD PTR [rsp + 40], rsi"); // spill readfds ptr + emitter.instruction("mov QWORD PTR [rsp + 48], rdx"); // spill writefds ptr + emitter.instruction("mov QWORD PTR [rsp + 56], r10"); // spill exceptfds ptr + emitter.instruction("mov QWORD PTR [rsp + 64], r8"); // spill timeout ptr + // -- build win_read fd_set from Linux readfds bitmap (rsi) -- + emitter.instruction("mov rax, QWORD PTR [rsp + 40]"); // readfds ptr + emitter.instruction("test rax, rax"); // NULL? + emitter.instruction("jz .Lpselect6_read_null"); // → pass NULL to select + emitter.instruction("lea rdi, [rsp + 112]"); // win_read fd_set base + emitter.instruction("xor eax, eax"); // fill value = 0 + emitter.instruction("mov rcx, 65"); // 65 qwords = 520 bytes + emitter.instruction("rep stosq"); // zero win_read fd_set (fd_count + array) + emitter.instruction("mov rax, QWORD PTR [rsp + 40]"); // reload readfds ptr (clobbered by rep stosq) + emitter.instruction("mov r11, QWORD PTR [rax]"); // Linux bitmap qword (bits 0..63) + emitter.instruction("lea rdi, [rsp + 112]"); // win_read fd_set base (for array writes) + emitter.instruction("xor r10d, r10d"); // bit counter b = 0 + emitter.label(".Lpselect6_read_loop"); + emitter.instruction("cmp r10d, 64"); // b < 64? + emitter.instruction("jge .Lpselect6_read_done"); // → done scanning bitmap + emitter.instruction("mov rdx, 1"); // rdx = 1 + emitter.instruction("mov ecx, r10d"); // shift count = b (32-bit mov zero-extends to rcx) + emitter.instruction("shl rdx, cl"); // rdx = 1 << b + emitter.instruction("test r11, rdx"); // bit b set in Linux bitmap? + emitter.instruction("jz .Lpselect6_read_next"); // → skip + emitter.instruction("mov ecx, DWORD PTR [rdi]"); // fd_count (u_int @0) + emitter.instruction("lea rax, [rip + _win_select_read_handles]"); // bit-to-SOCKET bridge table + emitter.instruction("mov rax, QWORD PTR [rax + r10 * 8]"); // recover full-width opaque SOCKET + emitter.instruction("mov QWORD PTR [rdi + 8 + rcx*8], rax"); // fd_array[fd_count] = SOCKET + emitter.instruction("inc ecx"); // fd_count++ + emitter.instruction("mov DWORD PTR [rdi], ecx"); // store updated fd_count + emitter.label(".Lpselect6_read_next"); + emitter.instruction("inc r10d"); // b++ + emitter.instruction("jmp .Lpselect6_read_loop"); // next bit + emitter.label(".Lpselect6_read_done"); + emitter.instruction("lea rax, [rsp + 112]"); // win_read ptr for select + emitter.instruction("mov QWORD PTR [rsp + 80], rax"); // store select arg2 + emitter.instruction("jmp .Lpselect6_read_end"); // skip NULL path + emitter.label(".Lpselect6_read_null"); + emitter.instruction("mov QWORD PTR [rsp + 80], 0"); // pass NULL for readfds + emitter.label(".Lpselect6_read_end"); + // -- build win_write fd_set from Linux writefds bitmap (rdx) -- + emitter.instruction("mov rax, QWORD PTR [rsp + 48]"); // writefds ptr + emitter.instruction("test rax, rax"); // NULL? + emitter.instruction("jz .Lpselect6_write_null"); // → pass NULL to select + emitter.instruction("lea rdi, [rsp + 632]"); // win_write fd_set base + emitter.instruction("xor eax, eax"); // fill value = 0 + emitter.instruction("mov rcx, 65"); // 65 qwords = 520 bytes + emitter.instruction("rep stosq"); // zero win_write fd_set + emitter.instruction("mov rax, QWORD PTR [rsp + 48]"); // reload writefds ptr + emitter.instruction("mov r11, QWORD PTR [rax]"); // Linux bitmap qword + emitter.instruction("lea rdi, [rsp + 632]"); // win_write fd_set base + emitter.instruction("xor r10d, r10d"); // bit counter b = 0 + emitter.label(".Lpselect6_write_loop"); + emitter.instruction("cmp r10d, 64"); // b < 64? + emitter.instruction("jge .Lpselect6_write_done"); // → done + emitter.instruction("mov rdx, 1"); // rdx = 1 + emitter.instruction("mov ecx, r10d"); // shift count = b (32-bit mov zero-extends to rcx) + emitter.instruction("shl rdx, cl"); // rdx = 1 << b + emitter.instruction("test r11, rdx"); // bit b set? + emitter.instruction("jz .Lpselect6_write_next"); // → skip + emitter.instruction("mov ecx, DWORD PTR [rdi]"); // fd_count + emitter.instruction("lea rax, [rip + _win_select_write_handles]"); // bit-to-SOCKET bridge table + emitter.instruction("mov rax, QWORD PTR [rax + r10 * 8]"); // recover full-width opaque SOCKET + emitter.instruction("mov QWORD PTR [rdi + 8 + rcx*8], rax"); // fd_array[fd_count] = SOCKET + emitter.instruction("inc ecx"); // fd_count++ + emitter.instruction("mov DWORD PTR [rdi], ecx"); // store fd_count + emitter.label(".Lpselect6_write_next"); + emitter.instruction("inc r10d"); // b++ + emitter.instruction("jmp .Lpselect6_write_loop"); // next bit + emitter.label(".Lpselect6_write_done"); + emitter.instruction("lea rax, [rsp + 632]"); // win_write ptr for select + emitter.instruction("mov QWORD PTR [rsp + 88], rax"); // store select arg3 + emitter.instruction("jmp .Lpselect6_write_end"); // skip NULL path + emitter.label(".Lpselect6_write_null"); + emitter.instruction("mov QWORD PTR [rsp + 88], 0"); // pass NULL for writefds + emitter.label(".Lpselect6_write_end"); + // -- build win_except fd_set from Linux exceptfds bitmap (r10) -- + emitter.instruction("mov rax, QWORD PTR [rsp + 56]"); // exceptfds ptr + emitter.instruction("test rax, rax"); // NULL? + emitter.instruction("jz .Lpselect6_except_null"); // → pass NULL to select + emitter.instruction("lea rdi, [rsp + 1152]"); // win_except fd_set base + emitter.instruction("xor eax, eax"); // fill value = 0 + emitter.instruction("mov rcx, 65"); // 65 qwords = 520 bytes + emitter.instruction("rep stosq"); // zero win_except fd_set + emitter.instruction("mov rax, QWORD PTR [rsp + 56]"); // reload exceptfds ptr + emitter.instruction("mov r11, QWORD PTR [rax]"); // Linux bitmap qword + emitter.instruction("lea rdi, [rsp + 1152]"); // win_except fd_set base + emitter.instruction("xor r10d, r10d"); // bit counter b = 0 + emitter.label(".Lpselect6_except_loop"); + emitter.instruction("cmp r10d, 64"); // b < 64? + emitter.instruction("jge .Lpselect6_except_done"); // → done + emitter.instruction("mov rdx, 1"); // rdx = 1 + emitter.instruction("mov ecx, r10d"); // shift count = b (32-bit mov zero-extends to rcx) + emitter.instruction("shl rdx, cl"); // rdx = 1 << b + emitter.instruction("test r11, rdx"); // bit b set? + emitter.instruction("jz .Lpselect6_except_next"); // → skip + emitter.instruction("mov ecx, DWORD PTR [rdi]"); // fd_count + emitter.instruction("lea rax, [rip + _win_select_except_handles]"); // bit-to-SOCKET bridge table + emitter.instruction("mov rax, QWORD PTR [rax + r10 * 8]"); // recover full-width opaque SOCKET + emitter.instruction("mov QWORD PTR [rdi + 8 + rcx*8], rax"); // fd_array[fd_count] = SOCKET + emitter.instruction("inc ecx"); // fd_count++ + emitter.instruction("mov DWORD PTR [rdi], ecx"); // store fd_count + emitter.label(".Lpselect6_except_next"); + emitter.instruction("inc r10d"); // b++ + emitter.instruction("jmp .Lpselect6_except_loop"); // next bit + emitter.label(".Lpselect6_except_done"); + emitter.instruction("lea rax, [rsp + 1152]"); // win_except ptr for select + emitter.instruction("mov QWORD PTR [rsp + 96], rax"); // store select arg4 + emitter.instruction("jmp .Lpselect6_except_end"); // skip NULL path + emitter.label(".Lpselect6_except_null"); + emitter.instruction("mov QWORD PTR [rsp + 96], 0"); // pass NULL for exceptfds + emitter.label(".Lpselect6_except_end"); + // -- build win_timeval from Linux struct timespec (r8) -- + emitter.instruction("mov rax, QWORD PTR [rsp + 64]"); // timeout ptr + emitter.instruction("test rax, rax"); // NULL? + emitter.instruction("jz .Lpselect6_tv_null"); // → pass NULL (block indefinitely) + emitter.instruction("mov rcx, QWORD PTR [rax]"); // sec (i64 @0) + emitter.instruction("mov DWORD PTR [rsp + 1672], ecx"); // tv_sec (low 32 bits @0) + emitter.instruction("mov rax, QWORD PTR [rax + 8]"); // nsec (i64 @8) + emitter.instruction("xor rdx, rdx"); // clear high half of dividend + emitter.instruction("mov ecx, 1000"); // divisor: 1000 (nsec → usec) + emitter.instruction("div rcx"); // rax = nsec / 1000 = tv_usec + emitter.instruction("mov DWORD PTR [rsp + 1676], eax"); // tv_usec (32-bit @4) + emitter.instruction("lea rax, [rsp + 1672]"); // win_timeval ptr + emitter.instruction("mov QWORD PTR [rsp + 104], rax"); // store select arg5 + emitter.instruction("jmp .Lpselect6_tv_end"); // skip NULL path + emitter.label(".Lpselect6_tv_null"); + emitter.instruction("mov QWORD PTR [rsp + 104], 0"); // pass NULL timeout (block) + emitter.label(".Lpselect6_tv_end"); + // -- materialize MSx64 args and call ws2_32 select -- + emitter.instruction("mov rcx, QWORD PTR [rsp + 32]"); // nfds (select arg1) + emitter.instruction("mov rdx, QWORD PTR [rsp + 80]"); // readfds (select arg2) + emitter.instruction("mov r8, QWORD PTR [rsp + 88]"); // writefds (select arg3) + emitter.instruction("mov r9, QWORD PTR [rsp + 96]"); // exceptfds (select arg4) + emitter.instruction("mov rax, QWORD PTR [rsp + 104]"); // win_timeval ptr + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // select arg5 (5th arg at [rsp+32]) + emitter.instruction("call select"); // ws2_32 select → eax (ready count or SOCKET_ERROR) + emitter.instruction("movsxd rax, eax"); // widen the signed Win32 int result before saving it + emitter.instruction("mov QWORD PTR [rsp + 72], rax"); // save result + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR is a 32-bit int sentinel + emitter.instruction("je .Lpselect6_error"); // → zero Linux bitmaps, return -1 + // -- success: writeback ready fds into the three Linux bitmaps -- + // -- read writeback: zero Linux bitmap, then set bits from win_read fd_array -- + emitter.instruction("mov rax, QWORD PTR [rsp + 40]"); // readfds ptr + emitter.instruction("test rax, rax"); // NULL (was not built)? + emitter.instruction("jz .Lpselect6_wb_read_skip"); // → nothing to write back + emitter.instruction("mov QWORD PTR [rax], 0"); // zero Linux read bitmap + emitter.instruction("lea rdi, [rsp + 112]"); // win_read fd_set base + emitter.instruction("mov r11d, DWORD PTR [rdi]"); // post-select fd_count + emitter.instruction("xor r10d, r10d"); // loop index i = 0 + emitter.label(".Lpselect6_wb_read_loop"); + emitter.instruction("cmp r10d, r11d"); // i < fd_count? + emitter.instruction("jge .Lpselect6_wb_read_done"); // → done + emitter.instruction("mov r9, QWORD PTR [rdi + 8 + r10*8]"); // fd = fd_array[i] (SOCKET, 8 bytes) + emitter.instruction("xor ecx, ecx"); // bridge slot search index + emitter.label(".Lpselect6_wb_read_find"); + emitter.instruction("cmp ecx, 64"); // searched every registered input slot? + emitter.instruction("jge .Lpselect6_wb_read_advance"); // ignore an unknown Winsock result + emitter.instruction("lea rax, [rip + _win_select_read_handles]"); // read-set SOCKET table + emitter.instruction("cmp QWORD PTR [rax + rcx * 8], r9"); // this slot owns the returned SOCKET? + emitter.instruction("je .Lpselect6_wb_read_found"); // set its original bitmap position + emitter.instruction("inc ecx"); // try next slot + emitter.instruction("jmp .Lpselect6_wb_read_find"); // continue reverse lookup + emitter.label(".Lpselect6_wb_read_found"); + emitter.instruction("mov rax, 1"); // rax = 1 + emitter.instruction("shl rax, cl"); // rax = 1 << fd + emitter.instruction("mov rdx, QWORD PTR [rsp + 40]"); // readfds ptr + emitter.instruction("or QWORD PTR [rdx], rax"); // set bit fd in Linux read bitmap + emitter.label(".Lpselect6_wb_read_advance"); + emitter.instruction("inc r10d"); // i++ + emitter.instruction("jmp .Lpselect6_wb_read_loop"); // next fd + emitter.label(".Lpselect6_wb_read_done"); + emitter.label(".Lpselect6_wb_read_skip"); + // -- write writeback: zero Linux bitmap, then set bits from win_write fd_array -- + emitter.instruction("mov rax, QWORD PTR [rsp + 48]"); // writefds ptr + emitter.instruction("test rax, rax"); // NULL? + emitter.instruction("jz .Lpselect6_wb_write_skip"); // → nothing to write back + emitter.instruction("mov QWORD PTR [rax], 0"); // zero Linux write bitmap + emitter.instruction("lea rdi, [rsp + 632]"); // win_write fd_set base + emitter.instruction("mov r11d, DWORD PTR [rdi]"); // post-select fd_count + emitter.instruction("xor r10d, r10d"); // loop index i = 0 + emitter.label(".Lpselect6_wb_write_loop"); + emitter.instruction("cmp r10d, r11d"); // i < fd_count? + emitter.instruction("jge .Lpselect6_wb_write_done"); // → done + emitter.instruction("mov r9, QWORD PTR [rdi + 8 + r10*8]"); // fd = fd_array[i] + emitter.instruction("xor ecx, ecx"); // bridge slot search index + emitter.label(".Lpselect6_wb_write_find"); + emitter.instruction("cmp ecx, 64"); // searched every registered input slot? + emitter.instruction("jge .Lpselect6_wb_write_advance"); // ignore an unknown Winsock result + emitter.instruction("lea rax, [rip + _win_select_write_handles]"); // write-set SOCKET table + emitter.instruction("cmp QWORD PTR [rax + rcx * 8], r9"); // this slot owns the returned SOCKET? + emitter.instruction("je .Lpselect6_wb_write_found"); // set its original bitmap position + emitter.instruction("inc ecx"); // try next slot + emitter.instruction("jmp .Lpselect6_wb_write_find"); // continue reverse lookup + emitter.label(".Lpselect6_wb_write_found"); + emitter.instruction("mov rax, 1"); // rax = 1 + emitter.instruction("shl rax, cl"); // rax = 1 << fd + emitter.instruction("mov rdx, QWORD PTR [rsp + 48]"); // writefds ptr + emitter.instruction("or QWORD PTR [rdx], rax"); // set bit fd in Linux write bitmap + emitter.label(".Lpselect6_wb_write_advance"); + emitter.instruction("inc r10d"); // i++ + emitter.instruction("jmp .Lpselect6_wb_write_loop"); // next fd + emitter.label(".Lpselect6_wb_write_done"); + emitter.label(".Lpselect6_wb_write_skip"); + // -- except writeback: zero Linux bitmap, then set bits from win_except fd_array -- + emitter.instruction("mov rax, QWORD PTR [rsp + 56]"); // exceptfds ptr + emitter.instruction("test rax, rax"); // NULL? + emitter.instruction("jz .Lpselect6_wb_except_skip"); // → nothing to write back + emitter.instruction("mov QWORD PTR [rax], 0"); // zero Linux except bitmap + emitter.instruction("lea rdi, [rsp + 1152]"); // win_except fd_set base + emitter.instruction("mov r11d, DWORD PTR [rdi]"); // post-select fd_count + emitter.instruction("xor r10d, r10d"); // loop index i = 0 + emitter.label(".Lpselect6_wb_except_loop"); + emitter.instruction("cmp r10d, r11d"); // i < fd_count? + emitter.instruction("jge .Lpselect6_wb_except_done"); // → done + emitter.instruction("mov r9, QWORD PTR [rdi + 8 + r10*8]"); // fd = fd_array[i] + emitter.instruction("xor ecx, ecx"); // bridge slot search index + emitter.label(".Lpselect6_wb_except_find"); + emitter.instruction("cmp ecx, 64"); // searched every registered input slot? + emitter.instruction("jge .Lpselect6_wb_except_advance"); // ignore an unknown Winsock result + emitter.instruction("lea rax, [rip + _win_select_except_handles]"); // exception-set SOCKET table + emitter.instruction("cmp QWORD PTR [rax + rcx * 8], r9"); // this slot owns the returned SOCKET? + emitter.instruction("je .Lpselect6_wb_except_found"); // set its original bitmap position + emitter.instruction("inc ecx"); // try next slot + emitter.instruction("jmp .Lpselect6_wb_except_find"); // continue reverse lookup + emitter.label(".Lpselect6_wb_except_found"); + emitter.instruction("mov rax, 1"); // rax = 1 + emitter.instruction("shl rax, cl"); // rax = 1 << fd + emitter.instruction("mov rdx, QWORD PTR [rsp + 56]"); // exceptfds ptr + emitter.instruction("or QWORD PTR [rdx], rax"); // set bit fd in Linux except bitmap + emitter.label(".Lpselect6_wb_except_advance"); + emitter.instruction("inc r10d"); // i++ + emitter.instruction("jmp .Lpselect6_wb_except_loop"); // next fd + emitter.label(".Lpselect6_wb_except_done"); + emitter.label(".Lpselect6_wb_except_skip"); + // -- common success return: rax = saved select result -- + emitter.instruction("mov rax, QWORD PTR [rsp + 72]"); // reload saved result + emitter.instruction("cdqe"); // sign-extend eax → rax (SOCKET_ERROR=-1 negative, see doc) + emitter.instruction("add rsp, 1688"); // restore stack + emitter.instruction("ret"); // return ready count (≥ 0) or -1 + // -- error path (SOCKET_ERROR): zero all non-NULL Linux bitmaps, return -1 -- + emitter.label(".Lpselect6_error"); + emitter.instruction("call __rt_wsa_capture_errno"); // preserve select's Winsock error before any other calls + emitter.instruction("mov rax, QWORD PTR [rsp + 40]"); // readfds ptr + emitter.instruction("test rax, rax"); // NULL? + emitter.instruction("jz .Lpselect6_err_w_skip"); // → skip + emitter.instruction("mov QWORD PTR [rax], 0"); // zero Linux read bitmap + emitter.label(".Lpselect6_err_w_skip"); + emitter.instruction("mov rax, QWORD PTR [rsp + 48]"); // writefds ptr + emitter.instruction("test rax, rax"); // NULL? + emitter.instruction("jz .Lpselect6_err_x_skip"); // → skip + emitter.instruction("mov QWORD PTR [rax], 0"); // zero Linux write bitmap + emitter.label(".Lpselect6_err_x_skip"); + emitter.instruction("mov rax, QWORD PTR [rsp + 56]"); // exceptfds ptr + emitter.instruction("test rax, rax"); // NULL? + emitter.instruction("jz .Lpselect6_err_ret"); // → skip + emitter.instruction("mov QWORD PTR [rax], 0"); // zero Linux except bitmap + emitter.label(".Lpselect6_err_ret"); + emitter.instruction("mov rax, -1"); // return -1 (SOCKET_ERROR) + emitter.instruction("add rsp, 1688"); // restore stack + emitter.instruction("ret"); // return -1 + emitter.blank(); +} + +/// Emits Linux-layout `sendmsg` scatter/gather lowering through `WSASend`. +pub(super) fn emit_shim_sendmsg(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_sendmsg"); + emitter.instruction("sub rsp, 120"); // WSASend stack args, WSABUF translation, and error locals + emitter.instruction("mov QWORD PTR [rsp + 56], rdi"); // SOCKET + emitter.instruction("mov QWORD PTR [rsp + 64], rsi"); // Linux msghdr pointer + emitter.instruction("mov QWORD PTR [rsp + 72], rdx"); // send flags + emitter.instruction("test rsi, rsi"); // msghdr present? + emitter.instruction("jz .Lsendmsg_invalid"); // EINVAL + emitter.instruction("cmp QWORD PTR [rsi], 0"); // msg_name unsupported by WSASend path + emitter.instruction("jne .Lsendmsg_unsupported"); // use sendto surface for addressed sends + emitter.instruction("cmp QWORD PTR [rsi + 32], 0"); // ancillary control data present? + emitter.instruction("jne .Lsendmsg_unsupported"); // Winsock extension path is not exposed here + emitter.instruction("mov rax, QWORD PTR [rsi + 24]"); // Linux msg_iovlen + emitter.instruction("test rax, rax"); // empty vector? + emitter.instruction("jz .Lsendmsg_empty"); // zero bytes sent + emitter.instruction("mov r9d, 0xffffffff"); // zero-extended DWORD maximum + emitter.instruction("cmp rax, r9"); // WSASend DWORD buffer count limit + emitter.instruction("ja .Lsendmsg_invalid"); // reject narrowing overflow + emitter.instruction("mov QWORD PTR [rsp + 88], rax"); // preserve buffer count + emitter.instruction("shl rax, 4"); // sizeof(WSABUF) = 16 + emitter.instruction("call __rt_heap_alloc"); // allocate Windows-specific WSABUF array + emitter.instruction("test rax, rax"); // allocation succeeded? + emitter.instruction("jz .Lsendmsg_nomem"); // ENOMEM + emitter.instruction("mov QWORD PTR [rsp + 80], rax"); // owned WSABUF array + emitter.instruction("mov QWORD PTR [rsp + 104], 0"); // translation index + emitter.label(".Lsendmsg_translate"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 104]"); // index + emitter.instruction("cmp rcx, QWORD PTR [rsp + 88]"); // translated every iovec? + emitter.instruction("jae .Lsendmsg_call"); // invoke Winsock + emitter.instruction("mov r10, QWORD PTR [rsp + 64]"); // msghdr + emitter.instruction("mov r10, QWORD PTR [r10 + 16]"); // Linux iovec array + emitter.instruction("mov r9, rcx"); // element index + emitter.instruction("shl r9, 4"); // byte offset = index * 16 + emitter.instruction("mov r11, QWORD PTR [r10 + r9 + 8]"); // iov_len + emitter.instruction("mov edx, 0xffffffff"); // zero-extended ULONG maximum + emitter.instruction("cmp r11, rdx"); // WSABUF.len is ULONG + emitter.instruction("ja .Lsendmsg_translate_invalid"); // reject narrowing overflow + emitter.instruction("mov rax, QWORD PTR [rsp + 80]"); // WSABUF base + emitter.instruction("mov DWORD PTR [rax + r9], r11d"); // WSABUF.len @ +0 + emitter.instruction("mov DWORD PTR [rax + r9 + 4], 0"); // explicit alignment padding + emitter.instruction("mov r11, QWORD PTR [r10 + r9]"); // iov_base + emitter.instruction("mov QWORD PTR [rax + r9 + 8], r11"); // WSABUF.buf @ +8 + emitter.instruction("add QWORD PTR [rsp + 104], 1"); // next element + emitter.instruction("jmp .Lsendmsg_translate"); // continue translation + emitter.label(".Lsendmsg_call"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 56]"); // SOCKET + emitter.instruction("mov rdx, QWORD PTR [rsp + 80]"); // WSABUF array + emitter.instruction("mov r8d, DWORD PTR [rsp + 88]"); // buffer count + emitter.instruction("lea r9, [rsp + 96]"); // lpNumberOfBytesSent + emitter.instruction("mov rax, QWORD PTR [rsp + 72]"); // flags + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // dwFlags (arg5) + emitter.instruction("mov QWORD PTR [rsp + 40], 0"); // lpOverlapped = NULL + emitter.instruction("mov QWORD PTR [rsp + 48], 0"); // completion routine = NULL + emitter.instruction("call WSASend"); // synchronous scatter/gather send + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je .Lsendmsg_native_fail"); // capture before cleanup + emitter.instruction("mov rax, QWORD PTR [rsp + 80]"); // owned WSABUF array + emitter.instruction("call __rt_heap_free"); // release translation + emitter.instruction("mov eax, DWORD PTR [rsp + 96]"); // bytes sent + emitter.instruction("add rsp, 120"); // restore stack + emitter.instruction("ret"); // return byte count + emitter.label(".Lsendmsg_native_fail"); + emitter.instruction("call WSAGetLastError"); // preserve would-block/timeout/native error + emitter.instruction("mov DWORD PTR [rsp + 112], eax"); // native error across cleanup + emitter.instruction("mov rax, QWORD PTR [rsp + 80]"); // owned WSABUF array + emitter.instruction("call __rt_heap_free"); // release translation + emitter.instruction("mov eax, DWORD PTR [rsp + 112]"); // restore WSA error + emitter.instruction("mov DWORD PTR [rip + __rt_wsa_last_error], eax"); // retain native state + emitter.instruction("call __rt_win32_errno_from_code"); // map timeout/nonblocking errors + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish errno + emitter.instruction("mov rax, -1"); // POSIX failure + emitter.instruction("add rsp, 120"); // restore stack + emitter.instruction("ret"); // return -1 + emitter.label(".Lsendmsg_translate_invalid"); + emitter.instruction("mov rax, QWORD PTR [rsp + 80]"); // owned WSABUF array + emitter.instruction("call __rt_heap_free"); // release partial translation + emitter.label(".Lsendmsg_invalid"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 22"); // EINVAL + emitter.instruction("jmp .Lsendmsg_direct_fail"); // return failure + emitter.label(".Lsendmsg_unsupported"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 95"); // EOPNOTSUPP for name/control surfaces + emitter.instruction("jmp .Lsendmsg_direct_fail"); // return failure + emitter.label(".Lsendmsg_nomem"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 12"); // ENOMEM + emitter.label(".Lsendmsg_direct_fail"); + emitter.instruction("mov rax, -1"); // POSIX failure + emitter.instruction("add rsp, 120"); // restore stack + emitter.instruction("ret"); // return -1 + emitter.label(".Lsendmsg_empty"); + emitter.instruction("xor eax, eax"); // empty vector sends zero bytes + emitter.instruction("add rsp, 120"); // restore stack + emitter.instruction("ret"); // return 0 + emitter.blank(); +} + +/// Emits Linux-layout `recvmsg` scatter/gather lowering through `WSARecv`. +pub(super) fn emit_shim_recvmsg(emitter: &mut Emitter) { + emit_shim_recvmsg_wsarecv(emitter); +} + +/// Translates Linux x86-64 `msghdr`/`iovec` storage into Winsock `WSABUF` +/// storage and performs a synchronous `WSARecv`, preserving Winsock errors +/// across temporary-buffer cleanup before publishing their POSIX equivalent. +fn emit_shim_recvmsg_wsarecv(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_recvmsg"); + emitter.instruction("sub rsp, 120"); // WSARecv stack args, WSABUF translation, and error locals + emitter.instruction("mov QWORD PTR [rsp + 56], rdi"); // SOCKET + emitter.instruction("mov QWORD PTR [rsp + 64], rsi"); // Linux msghdr pointer + emitter.instruction("mov DWORD PTR [rsp + 100], edx"); // input/output receive flags + emitter.instruction("test rsi, rsi"); // msghdr present? + emitter.instruction("jz .Lrecvmsg_invalid"); // EINVAL + emitter.instruction("cmp QWORD PTR [rsi], 0"); // msg_name unsupported by this connected-socket path + emitter.instruction("jne .Lrecvmsg_unsupported"); // use recvfrom surface for addressed receives + emitter.instruction("cmp QWORD PTR [rsi + 32], 0"); // ancillary control data present? + emitter.instruction("jne .Lrecvmsg_unsupported"); // Winsock extension path is not exposed here + emitter.instruction("mov rax, QWORD PTR [rsi + 24]"); // Linux msg_iovlen + emitter.instruction("test rax, rax"); // empty vector? + emitter.instruction("jz .Lrecvmsg_empty"); // zero bytes received + emitter.instruction("mov r9d, 0xffffffff"); // zero-extended DWORD maximum + emitter.instruction("cmp rax, r9"); // WSARecv DWORD buffer count limit + emitter.instruction("ja .Lrecvmsg_invalid"); // reject narrowing overflow + emitter.instruction("mov QWORD PTR [rsp + 88], rax"); // preserve buffer count + emitter.instruction("shl rax, 4"); // sizeof(WSABUF) = 16 + emitter.instruction("call __rt_heap_alloc"); // allocate Windows-specific WSABUF array + emitter.instruction("test rax, rax"); // allocation succeeded? + emitter.instruction("jz .Lrecvmsg_nomem"); // ENOMEM + emitter.instruction("mov QWORD PTR [rsp + 80], rax"); // owned WSABUF array + emitter.instruction("mov QWORD PTR [rsp + 104], 0"); // translation index + emitter.label(".Lrecvmsg_translate"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 104]"); // index + emitter.instruction("cmp rcx, QWORD PTR [rsp + 88]"); // translated every iovec? + emitter.instruction("jae .Lrecvmsg_call"); // invoke Winsock + emitter.instruction("mov r10, QWORD PTR [rsp + 64]"); // msghdr + emitter.instruction("mov r10, QWORD PTR [r10 + 16]"); // Linux iovec array + emitter.instruction("mov r9, rcx"); // element index + emitter.instruction("shl r9, 4"); // byte offset = index * 16 + emitter.instruction("mov r11, QWORD PTR [r10 + r9 + 8]"); // iov_len + emitter.instruction("mov edx, 0xffffffff"); // zero-extended ULONG maximum + emitter.instruction("cmp r11, rdx"); // WSABUF.len is ULONG + emitter.instruction("ja .Lrecvmsg_translate_invalid"); // reject narrowing overflow + emitter.instruction("mov rax, QWORD PTR [rsp + 80]"); // WSABUF base + emitter.instruction("mov DWORD PTR [rax + r9], r11d"); // WSABUF.len @ +0 + emitter.instruction("mov DWORD PTR [rax + r9 + 4], 0"); // explicit alignment padding + emitter.instruction("mov r11, QWORD PTR [r10 + r9]"); // iov_base + emitter.instruction("mov QWORD PTR [rax + r9 + 8], r11"); // WSABUF.buf @ +8 + emitter.instruction("add QWORD PTR [rsp + 104], 1"); // next element + emitter.instruction("jmp .Lrecvmsg_translate"); // continue translation + emitter.label(".Lrecvmsg_call"); + emitter.instruction("mov rcx, QWORD PTR [rsp + 56]"); // SOCKET + emitter.instruction("mov rdx, QWORD PTR [rsp + 80]"); // WSABUF array + emitter.instruction("mov r8d, DWORD PTR [rsp + 88]"); // buffer count + emitter.instruction("lea r9, [rsp + 96]"); // lpNumberOfBytesRecvd + emitter.instruction("lea rax, [rsp + 100]"); // lpFlags + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // flags pointer (arg5) + emitter.instruction("mov QWORD PTR [rsp + 40], 0"); // lpOverlapped = NULL + emitter.instruction("mov QWORD PTR [rsp + 48], 0"); // completion routine = NULL + emitter.instruction("call WSARecv"); // synchronous scatter/gather receive + emitter.instruction("cmp eax, -1"); // SOCKET_ERROR? + emitter.instruction("je .Lrecvmsg_native_fail"); // capture before cleanup + emitter.instruction("mov r10, QWORD PTR [rsp + 64]"); // Linux msghdr + emitter.instruction("mov eax, DWORD PTR [rsp + 100]"); // Winsock output flags + emitter.instruction("mov DWORD PTR [r10 + 48], eax"); // Linux msg_flags @ +48 + emitter.instruction("mov rax, QWORD PTR [rsp + 80]"); // owned WSABUF array + emitter.instruction("call __rt_heap_free"); // release translation + emitter.instruction("mov eax, DWORD PTR [rsp + 96]"); // bytes received + emitter.instruction("add rsp, 120"); // restore stack + emitter.instruction("ret"); // return byte count + emitter.label(".Lrecvmsg_native_fail"); + emitter.instruction("call WSAGetLastError"); // preserve would-block/timeout/native error + emitter.instruction("mov DWORD PTR [rsp + 112], eax"); // native error across cleanup + emitter.instruction("mov rax, QWORD PTR [rsp + 80]"); // owned WSABUF array + emitter.instruction("call __rt_heap_free"); // release translation + emitter.instruction("mov eax, DWORD PTR [rsp + 112]"); // restore WSA error + emitter.instruction("mov DWORD PTR [rip + __rt_wsa_last_error], eax"); // retain native state + emitter.instruction("call __rt_win32_errno_from_code"); // map timeout/nonblocking errors + emitter.instruction("mov DWORD PTR [rip + __rt_errno], eax"); // publish errno + emitter.instruction("mov rax, -1"); // POSIX failure + emitter.instruction("add rsp, 120"); // restore stack + emitter.instruction("ret"); // return -1 + emitter.label(".Lrecvmsg_translate_invalid"); + emitter.instruction("mov rax, QWORD PTR [rsp + 80]"); // owned WSABUF array + emitter.instruction("call __rt_heap_free"); // release partial translation + emitter.label(".Lrecvmsg_invalid"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 22"); // EINVAL + emitter.instruction("jmp .Lrecvmsg_direct_fail"); // return failure + emitter.label(".Lrecvmsg_unsupported"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 95"); // EOPNOTSUPP for name/control surfaces + emitter.instruction("jmp .Lrecvmsg_direct_fail"); // return failure + emitter.label(".Lrecvmsg_nomem"); + emitter.instruction("mov DWORD PTR [rip + __rt_errno], 12"); // ENOMEM + emitter.label(".Lrecvmsg_direct_fail"); + emitter.instruction("mov rax, -1"); // POSIX failure + emitter.instruction("add rsp, 120"); // restore stack + emitter.instruction("ret"); // return -1 + emitter.label(".Lrecvmsg_empty"); + emitter.instruction("mov DWORD PTR [rsi + 48], 0"); // no output flags for an empty vector + emitter.instruction("xor eax, eax"); // empty vector receives zero bytes + emitter.instruction("add rsp, 120"); // restore stack + emitter.instruction("ret"); // return 0 + emitter.blank(); +} + +/// Emits the W3e-2 net/dns/inet (ws2_32) + misc (msvcrt) `__rt_sys_*` shim +/// family: `getaddrinfo`, `freeaddrinfo`, `inet_pton`, `inet_ntop`, +/// `gethostbyaddr`, the protocol/service database lookups, `strtoll` (→ `_strtoi64`), `atof` (FP-return), `setlocale`, +/// and the unsupported Windows `chown`/`lchown` shims (see the `windows_c_shim_name` +/// doc-comment for why these are named `__rt_sys_libc_chown`/ +/// `__rt_sys_libc_lchown` rather than reusing the pre-existing +/// `__rt_sys_chown`/`__rt_sys_lchown` ENOSYS labels). `dup` reuses the +/// existing `emit_shim_dup_shims` `__rt_sys_dup` shim (no new shim needed). +pub(super) fn emit_shim_net_dns(emitter: &mut Emitter) { + emit_shim_getaddrinfo(emitter); + emit_shim_freeaddrinfo(emitter); + emit_shim_inet_pton(emitter); + emit_shim_inet_ntop(emitter); + emit_shim_gethostbyaddr_win(emitter); + emit_shim_getprotobyname(emitter); + emit_shim_getprotobynumber(emitter); + emit_shim_getservbyname(emitter); + emit_shim_getservbyport(emitter); + emit_shim_strtoll(emitter); + emit_shim_atof(emitter); + emit_shim_setlocale(emitter); + emit_shim_libc_chown_unsupported(emitter); +} + +/// Emits the `__rt_sys_getprotobyname` shim for Winsock's protocol database. +/// +/// SysV supplies the NUL-terminated name in `rdi`; Winsock expects it in `rcx` +/// and returns a transient `protoent*` in `rax`. +fn emit_shim_getprotobyname(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_getprotobyname"); + emitter.instruction("sub rsp, 40"); // reserve shadow space with MSx64 call alignment + emitter.instruction("mov rcx, rdi"); // move the protocol C string into MSx64 arg1 + emitter.instruction("call getprotobyname"); // query Winsock's protocol database + emitter.instruction("add rsp, 40"); // release the call frame + emitter.instruction("ret"); // return the transient protoent pointer or null + emitter.blank(); +} + +/// Emits the `__rt_sys_getprotobynumber` shim for Winsock's protocol database. +/// +/// The protocol id remains a 32-bit C `int`, so only `edi` is moved into the +/// first MSx64 integer argument register. +fn emit_shim_getprotobynumber(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_getprotobynumber"); + emitter.instruction("sub rsp, 40"); // reserve shadow space with MSx64 call alignment + emitter.instruction("mov ecx, edi"); // move the protocol number into MSx64 arg1 + emitter.instruction("call getprotobynumber"); // query Winsock's protocol database + emitter.instruction("add rsp, 40"); // release the call frame + emitter.instruction("ret"); // return the transient protoent pointer or null + emitter.blank(); +} + +/// Emits the `__rt_sys_getservbyname` shim for Winsock's services database. +/// +/// The service and protocol are independently NUL-terminated by the caller and +/// are shuffled from SysV `rdi`/`rsi` to MSx64 `rcx`/`rdx`. +fn emit_shim_getservbyname(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_getservbyname"); + emitter.instruction("sub rsp, 40"); // reserve shadow space with MSx64 call alignment + emitter.instruction("mov rdx, rsi"); // move the protocol C string into MSx64 arg2 + emitter.instruction("mov rcx, rdi"); // move the service C string into MSx64 arg1 + emitter.instruction("call getservbyname"); // query Winsock's services database + emitter.instruction("add rsp, 40"); // release the call frame + emitter.instruction("ret"); // return the transient servent pointer or null + emitter.blank(); +} + +/// Emits the `__rt_sys_getservbyport` shim for Winsock's services database. +/// +/// PHP exposes a host-order port while Winsock expects the low 16 bits in +/// network byte order, matching php-src's `htons((unsigned short) port)` call. +fn emit_shim_getservbyport(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_getservbyport"); + emitter.instruction("sub rsp, 40"); // reserve shadow space with MSx64 call alignment + emitter.instruction("mov eax, edi"); // retain the PHP host-order port in a 32-bit scratch register + emitter.instruction("xchg al, ah"); // convert the low 16 bits to network byte order without a CRT dependency + emitter.instruction("movzx ecx, ax"); // pass the widened network-order port as MSx64 arg1 + emitter.instruction("mov rdx, rsi"); // move the protocol C string into MSx64 arg2 + emitter.instruction("call getservbyport"); // query Winsock's services database + emitter.instruction("add rsp, 40"); // release the call frame + emitter.instruction("ret"); // return the transient servent pointer or null + emitter.blank(); +} + +/// Emits the `__rt_sys_getaddrinfo` shim: converts SysV `getaddrinfo(node, +/// service, *hints, **res)` (rdi, rsi, rdx, rcx) to MSx64 `getaddrinfo` +/// (rcx=node, rdx=service, r8=hints, r9=res). Register-shuffle hazard: SysV +/// arg4 (res) is in `rcx`, which is ALSO the MSx64 arg1 target, so it is +/// saved to `r9` BEFORE `rcx` is overwritten by the arg1 shuffle. SysV arg3 +/// (hints) is in `rdx`, which is ALSO MSx64 arg2, so it is saved to `r8` +/// BEFORE `rdx` is overwritten by the arg2 shuffle. `rdx`←`rsi` (service) and +/// `rcx`←`rdi` (node) move last. Returns an `int` status (0 success) in +/// `eax`; every consumer (`resolve_host_v6.rs:152`) does `test rax,rax; jnz` +/// — no cdqe (a nonzero error code stays nonzero whether or not it is +/// sign-extended; the check never distinguishes sign). +fn emit_shim_getaddrinfo(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_getaddrinfo"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov r9, rcx"); // SAVE res (SysV arg4) before rcx is overwritten + emitter.instruction("mov r8, rdx"); // SAVE hints (SysV arg3) before rdx is overwritten + emitter.instruction("mov rdx, rsi"); // service → arg2 (rdx) + emitter.instruction("mov rcx, rdi"); // node → arg1 (rcx) + emitter.instruction("call getaddrinfo"); // ws2_32 getaddrinfo (returns int status in eax) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return (no cdqe — status only test-for-nonzero) + emitter.blank(); +} + +/// Emits the `__rt_sys_freeaddrinfo` shim: converts SysV `freeaddrinfo(*res)` +/// (rdi) to MSx64 `freeaddrinfo` (rcx=res). Mirrors `emit_shim_zlib_trivial_1arg` +/// (1-arg case). `void` return — no cdqe. Sites: `resolve_host_v6.rs:171,180`. +fn emit_shim_freeaddrinfo(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_freeaddrinfo"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov rcx, rdi"); // res → arg1 (rcx) + emitter.instruction("call freeaddrinfo"); // ws2_32 freeaddrinfo (void return) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return (void — no cdqe) + emitter.blank(); +} + +/// Emits the `__rt_sys_inet_pton` shim: converts SysV `inet_pton(af, src, +/// dst)` (edi, rsi, rdx) to MSx64 `inet_pton` (ecx=af, rdx=src, r8=dst). +/// Register-shuffle hazard: SysV arg3 (dst) is in `rdx`, which is ALSO the +/// MSx64 arg2 (src) target, so it is saved to `r8` BEFORE `rdx` is +/// overwritten by the arg2 shuffle; `rdx`←`rsi` (src) and `ecx`←`edi` (af) +/// move last. Returns an `int` in `eax` (1 success, 0 fail, -1 +/// EAFNOSUPPORT); the consumer (`inet6_pton.rs:85`) does `cmp eax,1; sete +/// al` — collapses to a 0/1 predicate without ever sign-testing `rax`, so no +/// cdqe. +fn emit_shim_inet_pton(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_inet_pton"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov r8, rdx"); // SAVE dst (SysV arg3) before rdx is overwritten + emitter.instruction("mov rdx, rsi"); // src → arg2 (rdx) + emitter.instruction("mov ecx, edi"); // af → arg1 (ecx) + emitter.instruction("call inet_pton"); // ws2_32 inet_pton (returns int in eax) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return (no cdqe — cmp eax,1;sete never sign-tests) + emitter.blank(); +} + +/// Emits the `__rt_sys_inet_ntop` shim: converts SysV `inet_ntop(af, src, +/// dst, size)` (edi, rsi, rdx, ecx) to MSx64 `inet_ntop` (ecx=af, rdx=src, +/// r8=dst, r9d=size). Register-shuffle hazard: SysV arg4 (size) is in `ecx`, +/// which is ALSO the MSx64 arg1 (af) target, so it is saved to `r9d` BEFORE +/// `ecx` is overwritten by the arg1 shuffle. SysV arg3 (dst) is in `rdx`, +/// which is ALSO MSx64 arg2 (src), so it is saved to `r8` BEFORE `rdx` is +/// overwritten by the arg2 shuffle. `rdx`←`rsi` (src) and `ecx`←`edi` (af, +/// 32-bit family value) move last. Returns `const char*` (buf pointer or +/// NULL) in `rax`; the consumer (`format_sockaddr.rs:432`) does `test +/// rax,rax; jz` — pointer, never sign-tested, so no cdqe. +fn emit_shim_inet_ntop(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_inet_ntop"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov r9d, ecx"); // SAVE size (SysV arg4) before ecx is overwritten + emitter.instruction("mov r8, rdx"); // SAVE dst (SysV arg3) before rdx is overwritten + emitter.instruction("mov rdx, rsi"); // src → arg2 (rdx) + emitter.instruction("mov ecx, edi"); // af → arg1 (ecx, 32-bit family value) + emitter.instruction("call inet_ntop"); // ws2_32 inet_ntop (returns const char* buf or NULL in rax) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return pointer (no cdqe: never sign-tested) + emitter.blank(); +} + +/// Emits the `__rt_sys_gethostbyaddr` shim: converts SysV `gethostbyaddr(addr, +/// len, type)` (rdi, rsi, rdx) to MSx64 `gethostbyaddr` (rcx=addr, rdx=len, +/// r8=type). Register-shuffle hazard: SysV arg3 (type) is in `rdx`, which is +/// ALSO the MSx64 arg2 (len) target, so it is saved to `r8` BEFORE `rdx` is +/// overwritten by the arg2 shuffle; `rdx`←`rsi` (len) and `rcx`←`rdi` (addr) +/// move last. Returns `struct hostent*` (or NULL) in `rax`; the consumer +/// (`gethostbyaddr.rs:115`) does `test rax,rax; jz` — pointer, never +/// sign-tested, so no cdqe. Named `_win` to avoid colliding with the SysV +/// `emit_gethostbyaddr_linux_x86_64` runtime-helper function of a similar +/// name in `runtime::io::gethostbyaddr`. +fn emit_shim_gethostbyaddr_win(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_gethostbyaddr"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov r8, rdx"); // SAVE type (SysV arg3) before rdx is overwritten + emitter.instruction("mov rdx, rsi"); // len → arg2 (rdx) + emitter.instruction("mov rcx, rdi"); // addr → arg1 (rcx) + emitter.instruction("call gethostbyaddr"); // ws2_32 gethostbyaddr (returns struct hostent* or NULL in rax) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return pointer (no cdqe: never sign-tested) + emitter.blank(); +} + +/// Emits the `__rt_sys_strtoll` shim: converts SysV `strtoll(s, endptr, +/// base)` (rdi, rsi, edx) to the MSx64 msvcrt equivalent `_strtoi64` (rcx=s, +/// rdx=endptr, r8d=base) — msvcrt has no symbol literally named `strtoll`, +/// so this shim calls the differently-named `_strtoi64` import (no +/// self-recursion risk, unlike `atof`/`setlocale`/etc. which share their +/// SysV name with the msvcrt import). Register-shuffle hazard: SysV arg3 +/// (base) is in `edx`, which is ALSO the MSx64 arg2 (endptr) target, so it +/// is saved to `r8d` BEFORE `edx` is overwritten by the arg2 shuffle. +/// `rdx`←`rsi` (endptr) and `rcx`←`rdi` (s) move last. Returns a 64-bit +/// `long long` in `rax` (LLONG_MAX/MIN on overflow) — full 64-bit value, +/// never a 32-bit status needing sign-extension, so no cdqe. Site: +/// `str_to_int.rs:94`. +fn emit_shim_strtoll(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_strtoll"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov r8, rdx"); // SAVE base (SysV arg3) before rdx is overwritten + emitter.instruction("mov rdx, rsi"); // endptr → arg2 (rdx) + emitter.instruction("mov rcx, rdi"); // s → arg1 (rcx) + emitter.instruction("call _strtoi64"); // msvcrt _strtoi64 (returns 64-bit long long in rax) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return (no cdqe: full 64-bit value, not a status) + emitter.blank(); +} + +/// Emits the `__rt_sys_atof` shim: converts SysV `atof(s)` (rdi) to MSx64 +/// `atof` (rcx=s). Unlike the uniform [`emit_fp_shadow_shim`] family (`pow`, +/// `sin`, ... — all-FP-register arguments, identical in SysV and MSx64), `atof` +/// takes a POINTER argument, which SysV passes in `rdi` but MSx64 expects in +/// `rcx` — so this shim needs its own `rdi`→`rcx` move before the call +/// (`emit_fp_shadow_shim` cannot be reused here). The `double` result stays in +/// `xmm0` in both ABIs — xmm0 is NEVER touched by this shim. Sites: +/// `mixed_cast_float.rs:110`, `json_decode_mixed/x86_64.rs:455` +/// (`mixed_cast_float.rs:57` is the AArch64 branch of the same function — +/// left untouched). +fn emit_shim_atof(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_atof"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov rcx, rdi"); // s → arg1 (rcx); xmm0 untouched + emitter.instruction("call atof"); // msvcrt atof (returns double in xmm0) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return (double stays in xmm0 — no cdqe, not an int result) + emitter.blank(); +} + +/// Emits the `__rt_sys_setlocale` shim: converts SysV `setlocale(category, +/// locale)` (edi, rsi) to MSx64 `setlocale` (ecx=category, rdx=locale). +/// `rdx`←`rsi` (locale) and `ecx`←`edi` (category) never collide with an +/// earlier MSx64 write, so the shuffle order does not matter. Returns +/// `char*` (or NULL) in `rax`; the consumer (`regex_locale.rs:50,55`) does +/// `test rax,rax; jnz` — pointer, never sign-tested, so no cdqe. +fn emit_shim_setlocale(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_setlocale"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov rdx, rsi"); // locale → arg2 (rdx) + emitter.instruction("mov ecx, edi"); // category → arg1 (ecx) + emitter.instruction("call setlocale"); // msvcrt setlocale (returns char* or NULL in rax) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return pointer (no cdqe: never sign-tested) + emitter.blank(); +} + +/// Emits the `__rt_sys_libc_chown`/`__rt_sys_libc_lchown` shims. +/// +/// Windows has no POSIX ownership change. php-src omits `lchown` and `lchgrp` +/// there, while elephc still needs internal failure shims so its shared runtime +/// assembly contains no nonexistent CRT imports. `chown` and `chgrp` remain +/// PHP-visible and lower through the same `-1` failure path. These shims are +/// separate from the pre-existing `__rt_sys_chown`/`__rt_sys_lchown` +/// (`emit_shim_c_symbol_delegates`), which serve the unrelated transformed +/// Linux-syscall-number 92/94 path. +pub(super) fn emit_shim_libc_chown_unsupported(emitter: &mut Emitter) { + for label in ["__rt_sys_libc_chown", "__rt_sys_libc_lchown"] { + emitter.label_global(label); + emitter.instruction("mov rax, -1"); // PHP_WIN32 rejects ownership changes on plain filesystem paths + emitter.instruction("ret"); // return failure without touching the path or errno + emitter.blank(); + } +} diff --git a/src/codegen_support/runtime/win32/shims_pcre.rs b/src/codegen_support/runtime/win32/shims_pcre.rs new file mode 100644 index 0000000000..79df5f7726 --- /dev/null +++ b/src/codegen_support/runtime/win32/shims_pcre.rs @@ -0,0 +1,139 @@ +//! Win32 shims for the PCRE2-POSIX family (W3e-1) plus msvcrt malloc/free. + +use crate::codegen::emit::Emitter; + +/// Emits the W3e-1 PCRE2-POSIX family of `__rt_sys_*` shims: `pcre2_regcomp`, +/// `pcre2_regexec`, `pcre2_regfree`. Like the W3d zlib family (see +/// [`emit_shim_zlib`]), these wrap symbols statically linked from the +/// MinGW-sysroot `libpcre2-posix.a`/`libpcre2-8.a` (via `ELEPHC_MINGW_SYSROOT`, +/// `src/linker.rs:255`), which is MSx64 ABI — NOT SysV — because it is built +/// by MinGW gcc targeting Windows, exactly like every other Win32-side symbol +/// this module shims. +/// +/// Return-value cdqe verdict (Class-3 sign-extension rule): `pcre2_regcomp` +/// and `pcre2_regexec` both return an `int` status where 0 means +/// success/match and nonzero means failure/no-match. EVERY current runtime +/// consumer tests this return with `test eax, eax` followed by `jnz`/`jz` +/// (equality against zero), never `js`/`jl` (sign test) — verified against +/// every x86_64 call site in `preg_match.rs` (`:365-366`, `:434-435`, +/// `:464-465`), `preg_split.rs` (regcomp/regexec status checks in +/// `emit_preg_split_linux_x86_64`), `preg_replace.rs` (`:337`/`:364-365`), +/// and `preg_replace_callback.rs` (regcomp/regexec status checks in +/// `emit_preg_replace_callback_linux_x86_64`) — so a `cdqe`-less negative +/// status (which zero-extends into a nonzero `rax`) is still correctly +/// reported as failure by every `test`/`jnz` consumer. No shim below performs +/// `cdqe`. `pcre2_regfree` returns `void`, so cdqe is moot there too. +pub(super) fn emit_shim_pcre2_posix(emitter: &mut Emitter) { + emit_shim_pcre2_regcomp(emitter); + emit_shim_pcre2_regexec(emitter); + emit_shim_pcre2_regfree(emitter); +} + +/// Emits the `__rt_sys_pcre2_regcomp` shim: converts SysV +/// `pcre2_regcomp(regex_t* preg, const char* pattern, int cflags)` (rdi, rsi, +/// edx) to MSx64 `pcre2_regcomp` (rcx=preg, rdx=pattern, r8d=cflags). +/// Register-shuffle hazard: SysV arg3 (cflags) is in `edx`, which is ALSO the +/// MSx64 arg2 target, so it is saved to `r8` BEFORE `rdx` is overwritten by +/// the arg2 shuffle (rsi→rdx); `rdi`→`rcx` moves last since it never +/// collides with an earlier MSx64 write. No cdqe: see +/// [`emit_shim_pcre2_posix`] for the family-wide cdqe verdict — the +/// `test eax,eax; jnz/jz` consumers only distinguish zero from nonzero. +fn emit_shim_pcre2_regcomp(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_pcre2_regcomp"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov r8, rdx"); // SAVE cflags (SysV arg3) before rdx is overwritten + emitter.instruction("mov rdx, rsi"); // pattern → arg2 (rdx) + emitter.instruction("mov rcx, rdi"); // preg → arg1 (rcx) + emitter.instruction("call pcre2_regcomp"); // libpcre2-posix pcre2_regcomp (MSx64 ABI, returns int in eax) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return (no cdqe — see emit_shim_pcre2_posix) + emitter.blank(); +} + +/// Emits the `__rt_sys_pcre2_regexec` shim: converts SysV `pcre2_regexec(const +/// regex_t* preg, const char* string, size_t nmatch, regmatch_t* pmatch, int +/// eflags)` (rdi, rsi, rdx, rcx, r8d) to MSx64 `pcre2_regexec` (rcx=preg, +/// rdx=string, r8=nmatch, r9=pmatch, `[rsp+32]`=eflags — the 5th arg, on the +/// stack above the 32-byte shadow space). This is the only 5-argument shim in +/// the W3e-1 family, so both register AND stack-arg placement matter. +/// +/// Register-shuffle order, in the ORDER the shim executes it (each SysV +/// register is read into its MSx64 target or a scratch register BEFORE being +/// overwritten by an earlier-numbered MSx64 argument): SysV arg5 (eflags) is +/// in `r8`, which is ALSO the MSx64 arg3 (nmatch) target, so it is saved to +/// `r10` and spilled to its `[rsp+32]` stack slot FIRST, before `r8` is +/// overwritten by the arg3 shuffle. SysV arg3 (nmatch) is in `rdx`, which is +/// ALSO MSx64 arg2 (string), so it is saved to `r8` (now free) BEFORE `rdx` +/// is overwritten by the arg2 shuffle. SysV arg4 (pmatch) is in `rcx`, which +/// is ALSO MSx64 arg1 (preg), so it is saved to `r9` BEFORE `rcx` is +/// overwritten by the arg1 shuffle. `rdx`←`rsi` (string) and `rcx`←`rdi` +/// (preg) move last since `rdi`/`rsi` never collide with an earlier MSx64 +/// write. +/// +/// Alignment: shim entry `rsp ≡ 8 (mod 16)` (the universal post-`call` +/// convention every shim in this module assumes). `sub rsp, 56` reserves +/// shadow(32) + the 5th-arg slot(8) + 16 bytes of padding — `56 ≡ 8 (mod +/// 16)`, so `rsp` lands exactly on a 16-byte boundary at `call +/// pcre2_regexec`. No cdqe: see [`emit_shim_pcre2_posix`] for the family-wide +/// cdqe verdict. +fn emit_shim_pcre2_regexec(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_pcre2_regexec"); + emitter.instruction("sub rsp, 56"); // shadow(32) + 5th-arg slot(8) + pad(16), 16-byte aligned + emitter.instruction("mov r10, r8"); // SAVE eflags (SysV arg5/r8) before r8 is overwritten + emitter.instruction("mov QWORD PTR [rsp + 32], r10"); // eflags → MSx64 5th arg (stack slot above shadow) + emitter.instruction("mov r8, rdx"); // SAVE nmatch (SysV arg3/rdx) before rdx is overwritten + emitter.instruction("mov r9, rcx"); // SAVE pmatch (SysV arg4/rcx) before rcx is overwritten + emitter.instruction("mov rcx, rdi"); // preg → arg1 (rcx) + emitter.instruction("mov rdx, rsi"); // string → arg2 (rdx) + emitter.instruction("call pcre2_regexec"); // libpcre2-posix pcre2_regexec (MSx64 ABI, returns int in eax) + emitter.instruction("add rsp, 56"); // restore stack + emitter.instruction("ret"); // return (no cdqe — see emit_shim_pcre2_posix) + emitter.blank(); +} + +/// Emits the `__rt_sys_pcre2_regfree` shim: converts SysV +/// `pcre2_regfree(regex_t* preg)` (rdi) to MSx64 `pcre2_regfree` (rcx=preg). +/// Mirrors `emit_shim_zlib_trivial_1arg` (1-arg case). `pcre2_regfree` +/// returns `void`, so no cdqe is possible or needed — see +/// [`emit_shim_pcre2_posix`] for the family-wide cdqe verdict. +fn emit_shim_pcre2_regfree(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_pcre2_regfree"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov rcx, rdi"); // preg → arg1 (rcx) + emitter.instruction("call pcre2_regfree"); // libpcre2-posix pcre2_regfree (MSx64 ABI, void return) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return (void — no cdqe) + emitter.blank(); +} + +/// Emits the `__rt_sys_malloc` shim: converts SysV `malloc(size_t size)` +/// (rdi) to MSx64 `malloc` (rcx=size), calling the standard msvcrt `malloc` +/// import. Mirrors `emit_shim_zlib_trivial_1arg` (1-arg case). The runtime +/// uses this to allocate the dynamic `regmatch_t` capture vector for PCRE2 +/// regexec calls (NOT the PHP heap — `__rt_heap_alloc` is a separate, +/// unrelated allocator). The pointer return stays in `rax` (never +/// sign-tested), so no cdqe. +pub(super) fn emit_shim_malloc(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_malloc"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov rcx, rdi"); // size → arg1 (rcx) + emitter.instruction("call malloc"); // msvcrt malloc (returns void* in rax) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return pointer (no cdqe: never sign-tested) + emitter.blank(); +} + +/// Emits the `__rt_sys_free` shim: converts SysV `free(void* ptr)` (rdi) to +/// MSx64 `free` (rcx=ptr), calling the standard msvcrt `free` import. Mirrors +/// `emit_shim_zlib_trivial_1arg` (1-arg case). Frees the dynamic +/// `regmatch_t` capture vector allocated by `__rt_sys_malloc` (see +/// [`emit_shim_malloc`]). `free` returns `void`, so no cdqe. +pub(super) fn emit_shim_free(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_free"); + emitter.instruction("sub rsp, 40"); // shadow(32) + alignment(8) + emitter.instruction("mov rcx, rdi"); // ptr → arg1 (rcx) + emitter.instruction("call free"); // msvcrt free (void return) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return (void — no cdqe) + emitter.blank(); +} diff --git a/src/codegen_support/runtime/win32/shims_time.rs b/src/codegen_support/runtime/win32/shims_time.rs new file mode 100644 index 0000000000..4a19abd507 --- /dev/null +++ b/src/codegen_support/runtime/win32/shims_time.rs @@ -0,0 +1,710 @@ +//! Win32 shims for the time/env family: clock_gettime, getenv/putenv/tzset, +//! time/localtime/gmtime/mktime/gettimeofday, getrusage, clock_getres. +//! +//! Key details: +//! - `emit_shim_localtime`/`emit_shim_mktime` additionally resolve the active +//! default timezone's UTC offset through the elephc-tz bridge (WF9): when a +//! date/mktime/strtotime call site has published `elephc_tz_offset` into the +//! `_elephc_tz_offset_fn` slot (`crate::codegen_support::tz_bridge`), these +//! shims use it to compute a real IANA-correct offset instead of relying on +//! msvcrt's TZ-only, no-zoneinfo `localtime`/`mktime`. See each function's +//! docblock for the exact fallback contract when the slot is null or the zone +//! is unresolvable. + +use crate::codegen::emit::Emitter; + +/// The `i64::MIN` "no data" sentinel `elephc_tz_offset` returns for an unknown or +/// false zone (see `crates/elephc-tz/src/abi.rs`), as a GAS hex literal: no real +/// packed `(offset, isdst)` value can equal it, since real UTC offsets are many +/// orders of magnitude smaller than `i64::MIN / 2`. +const TZ_OFFSET_SENTINEL_HEX: &str = "0x8000000000000000"; + +/// Emits the Win32 implementation of `clock_gettime(clock_id, timespec*)`. +/// +/// SysV input is `rdi = clock_id`, `rsi = timespec*`. `CLOCK_MONOTONIC` (the +/// Linux-compatible id `1` emitted by the shared x86_64 runtime) is backed by +/// `QueryPerformanceCounter`; every other id uses `GetSystemTimeAsFileTime` as +/// the realtime clock. `rsi` is nonvolatile in the Win64 ABI, so the Win32 calls +/// preserve the output pointer. +pub(super) fn emit_shim_clock_gettime(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_clock_gettime"); + emitter.instruction("cmp edi, 1"); // CLOCK_MONOTONIC uses the Linux-compatible id emitted by __rt_hrtime + emitter.instruction("je .Lclock_gettime_monotonic"); // route monotonic requests through QueryPerformanceCounter + emitter.instruction("sub rsp, 40"); // shadow(32) + FILETIME(8) + emitter.instruction("lea rcx, [rsp + 32]"); // &filetime + emitter.instruction("call GetSystemTimeAsFileTime"); // get 100ns intervals since 1601 + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // load FILETIME (64-bit) + emitter.instruction("mov r10, 116444736000000000"); // Unix epoch offset (100ns intervals from 1601 to 1970) + emitter.instruction("sub rax, r10"); // convert to Unix epoch (100ns intervals since 1970) + emitter.instruction("xor rdx, rdx"); // clear high 64 bits of dividend + emitter.instruction("mov r11, 10000000"); // divisor: 100ns intervals per second + emitter.instruction("div r11"); // RDX:RAX / r11 → RAX = seconds, RDX = remainder + emitter.instruction("mov QWORD PTR [rsi], rax"); // store realtime seconds + emitter.instruction("imul rdx, 100"); // convert remainder to nanoseconds + emitter.instruction("mov QWORD PTR [rsi + 8], rdx"); // store realtime nanoseconds + emitter.instruction("xor rax, rax"); // return 0 (success) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return + + emitter.label(".Lclock_gettime_monotonic"); + emitter.instruction("sub rsp, 56"); // shadow(32) + counter/frequency qwords + alignment + emitter.instruction("lea rcx, [rsp + 32]"); // &counter + emitter.instruction("call QueryPerformanceCounter"); // read the monotonic high-resolution counter + emitter.instruction("test eax, eax"); // did QueryPerformanceCounter succeed? + emitter.instruction("jz .Lclock_gettime_error"); // no -> report failure without publishing uninitialized data + emitter.instruction("lea rcx, [rsp + 40]"); // &frequency + emitter.instruction("call QueryPerformanceFrequency"); // read ticks per second + emitter.instruction("test eax, eax"); // did QueryPerformanceFrequency succeed? + emitter.instruction("jz .Lclock_gettime_error"); // no -> report failure + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // rax = counter ticks + emitter.instruction("xor rdx, rdx"); // clear the high dividend word + emitter.instruction("div QWORD PTR [rsp + 40]"); // rax = whole seconds, rdx = remaining ticks + emitter.instruction("mov QWORD PTR [rsi], rax"); // store monotonic seconds + emitter.instruction("imul rax, rdx, 1000000000"); // scale the sub-second remainder to nanosecond ticks + emitter.instruction("xor rdx, rdx"); // clear the high dividend word + emitter.instruction("div QWORD PTR [rsp + 40]"); // rax = monotonic nanoseconds within the second + emitter.instruction("mov QWORD PTR [rsi + 8], rax"); // store monotonic nanoseconds + emitter.instruction("xor eax, eax"); // return 0 (success) + emitter.instruction("add rsp, 56"); // restore stack + emitter.instruction("ret"); // return + emitter.label(".Lclock_gettime_error"); + emitter.instruction("mov rax, -1"); // return -1 when the performance counter is unavailable + emitter.instruction("add rsp, 56"); // restore stack + emitter.instruction("ret"); // return failure + emitter.blank(); +} + +/// Emits a shim that wraps msvcrt `getenv(name)`: SysV `rdi`=name pointer → MSx64 +/// `rcx`=name pointer, single argument, one-instruction shuffle. Return value (a +/// `char*` pointer, or NULL when unset) passes through unmodified in `rax` — no +/// `cdqe`, since this is a pointer, not a sign-tested int32 status. +pub(super) fn emit_shim_getenv(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_getenv"); + emitter.instruction("sub rsp, 40"); // shadow space + emitter.instruction("mov rcx, rdi"); // name + emitter.instruction("call getenv"); // msvcrt getenv(name) -> char* or NULL + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return (rax unchanged) + emitter.blank(); +} + +/// Emits a shim that wraps msvcrt `putenv(string)`: SysV `rdi`=assignment string +/// pointer → MSx64 `rcx`=assignment string pointer, single argument, one-instruction +/// shuffle. Return value (int status, 0 on success) passes through unmodified in +/// `rax`; callers that sign-test/compare it (e.g. `cmp rax, 0`) still see the correct +/// low bits. +pub(super) fn emit_shim_putenv(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_putenv"); + emitter.instruction("sub rsp, 40"); // shadow space + emitter.instruction("mov rcx, rdi"); // "NAME=value" assignment string + emitter.instruction("call _putenv"); // msvcrt _putenv(string) -> int status + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return status in rax + emitter.blank(); +} + +/// Emits a shim that wraps msvcrt `tzset(void)`: zero arguments, so no register +/// shuffle is needed — the shim exists purely to route the call through the +/// `emit_call_c`/`windows_c_shim_name` registry instead of a bare `call tzset`, +/// which would be wrong for any future *argumented* Windows datetime call added to +/// this call site's callers on the strength of "tzset needs no ABI fixup". +pub(super) fn emit_shim_tzset(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_tzset"); + emitter.instruction("sub rsp, 40"); // shadow space + emitter.instruction("call _tzset"); // msvcrt _tzset(void) -> re-reads TZ + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits a shim that wraps msvcrt `time(time_t*)`: SysV `rdi`=time_t* (or NULL) → +/// MSx64 `rcx`=time_t*, single argument, one-instruction shuffle. Return value +/// (`time_t`, seconds since epoch) passes through unmodified in `rax` — no `cdqe`, +/// since `time_t` is a 64-bit quantity here, not a sign-tested int32 status. +/// +/// ## time_t width +/// MinGW-w64's `libmsvcrt.a` resolves the bare `time` symbol to a one-instruction +/// `jmp [rip+__imp_time]` thunk importing from `api-ms-win-crt-time-l1-1-0.dll` +/// (the Universal-CRT time forwarder that ships on every supported Windows target), +/// verified by disassembling the archive member that defines it — NOT a legacy +/// 32-bit `__time32_t` symbol. On 64-bit Windows `time_t` is always the 64-bit +/// `__time64_t` encoding, so the bare name is used directly; no `_time64` routing +/// is needed. +pub(super) fn emit_shim_time(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_time"); + emitter.instruction("sub rsp, 40"); // shadow space + emitter.instruction("mov rcx, rdi"); // time_t* out-param (or NULL) + emitter.instruction("call time"); // msvcrt time(tloc) -> time_t (64-bit) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return time_t in rax + emitter.blank(); +} + +/// Number of seconds in one proleptic-Gregorian 400-year cycle (146097 days): +/// `146097 * 86400`. The calendar (year/month/day/weekday/leap pattern) repeats +/// exactly every such cycle, which both this module's `__rt_win_safe_gmtime` and +/// `crate::codegen_support::runtime::system::mktime` (the construction-direction +/// counterpart) rely on. +const GREGORIAN_CYCLE_SECONDS: i64 = 146_097 * 86_400; + +/// Emits `__rt_win_safe_gmtime`: a drop-in, same-ABI replacement for msvcrt +/// `gmtime(const time_t*)` (MSx64: `rcx`=time_t*, returns `struct tm*` in `rax`) +/// that stays correct outside msvcrt/UCRT's own supported decomposition window. +/// +/// ## Why +/// Microsoft's UCRT `gmtime`/`localtime`/`_gmtime64`/`_localtime64` only support +/// timestamps from `1970-01-01T00:00:00Z` through `3000-12-31T23:59:59Z` +/// (documented range) and return `NULL` outside it — unlike glibc, which +/// decomposes the full `time_t` range. elephc allows PHP programs to format +/// arbitrary (including pre-1970 and far-future/BCE-proleptic) timestamps via +/// `date()`/`gmdate()`, so a caller that blindly dereferences msvcrt's result +/// crashes on any such value (surfaced under Wine as an unrecoverable hang, not +/// a clean non-zero exit, since the crash trips Wine's unhandled-exception path +/// rather than terminating the process outright). +/// +/// ## How +/// Shifts the input timestamp by a whole number of Gregorian 400-year cycles +/// (`N = floor(ts / GREGORIAN_CYCLE_SECONDS)`, via a truncating `idiv` corrected +/// to floor/Euclidean semantics when the remainder's sign disagrees with the +/// positive divisor — the same floor-vs-truncating fixup +/// `crates/elephc-tz`'s `days_and_secs` performs in Rust) so the shifted +/// timestamp always lands in `[0, GREGORIAN_CYCLE_SECONDS)` — a ~400-year window +/// starting at the epoch, comfortably inside UCRT's supported range regardless +/// of how far `ts` originally was in the past or future. Calls the real msvcrt +/// `gmtime` on the shifted value (always safe, always non-`NULL`), then adds +/// `N * 400` back onto the returned `tm_year` (`+20` in msvcrt's own struct tm, +/// which this shim mutates in place before returning msvcrt's own pointer +/// unchanged) — the only field that differs between the two dates, since the +/// 400-year cycle exactly repeats month/day/weekday/leap-year pattern +/// (146097 days is exactly divisible by 7). +/// +/// Every caller in this module that used to `call gmtime` directly on a +/// timestamp that PHP code controls (not a fixed "now" value) should call this +/// instead: [`emit_shim_gmtime`] (`__rt_sys_gmtime`, backing `gmdate()`) and the +/// bridge path in [`emit_shim_localtime`] (`__rt_sys_localtime`, backing +/// `date()`'s local decomposition of `ts + offset`). +pub(super) fn emit_shim_win_safe_gmtime(emitter: &mut Emitter) { + emitter.label_global("__rt_win_safe_gmtime"); + emitter.instruction("push rbp"); // preserve the caller frame pointer before the shift/unshift locals + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the saved cycle count + emitter.instruction("sub rsp, 48"); // 32-byte MSx64 shadow space + 8-byte shifted-ts slot + 8-byte saved-N slot (16-byte aligned) + emitter.instruction("mov rax, QWORD PTR [rcx]"); // rax = ts (*time_t) + emitter.instruction(&format!("movabs r10, {}", GREGORIAN_CYCLE_SECONDS)); // r10 = GREGORIAN_CYCLE_SECONDS (146097 * 86400) + emitter.instruction("cqo"); // sign-extend rax into rdx:rax for the signed 128-bit-by-64-bit divide + emitter.instruction("idiv r10"); // rax = trunc(ts / cycle), rdx = remainder (sign follows ts, per x86 idiv) + emitter.instruction("test rdx, rdx"); // truncating remainder already floor-correct (>= 0)? + emitter.instruction("jns .Lwin_safe_gmtime_no_adjust"); // yes -> shifted_ts is already in [0, cycle) + emitter.instruction("dec rax"); // floor-correct: one fewer whole cycle... + emitter.instruction("add rdx, r10"); // ...and fold that cycle back into the remainder, landing it in [0, cycle) + emitter.label(".Lwin_safe_gmtime_no_adjust"); + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save N (the cycle count) across the libc call + emitter.instruction("mov QWORD PTR [rsp + 32], rdx"); // stage shifted_ts (always UCRT-safe: within one cycle of the epoch) + emitter.instruction("lea rcx, [rsp + 32]"); // &shifted_ts + emitter.instruction("call gmtime"); // msvcrt gmtime(&shifted_ts) -> rax = its own static struct tm* (never NULL here) + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload N + emitter.instruction("imul r10d, r10d, 400"); // N * 400 years (fits comfortably in 32 bits for any realistic timestamp) + emitter.instruction("add DWORD PTR [rax + 20], r10d"); // tm_year += N*400, undoing the cycle shift (only field the shift changes) + emitter.instruction("add rsp, 48"); // release the frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return struct tm* (msvcrt's own buffer, tm_year corrected) in rax + emitter.blank(); +} + +/// Emits a shim that resolves local time for `localtime(const time_t*)` via the +/// elephc-tz IANA offset bridge (WF9) when available, falling back to plain +/// msvcrt `localtime()` otherwise. SysV `rdi`=time_t*; returns a `struct tm*` in +/// `rax`, matching the plain-passthrough shim it replaces. +/// +/// ## Bridge path +/// If a date/mktime/strtotime call site has published `elephc_tz_offset` into +/// `_elephc_tz_offset_fn` (`crate::codegen_support::tz_bridge`), this: resolves +/// the active default timezone's identifier the same way +/// `__rt_date_default_timezone_get` does (`_php_default_tz_len` / +/// `_php_tz_env+3`, or `_php_tz_utc`); calls `elephc_tz_offset(name, ts)` +/// through the slot (MSx64-corrected via `Emitter::emit_native_bridge_call`); +/// and — when it resolves (not the `i64::MIN` sentinel) — decomposes +/// `ts + offset` via [`emit_shim_win_safe_gmtime`]'s `__rt_win_safe_gmtime` +/// (UTC decomposition needs no zoneinfo, and staying correct outside msvcrt's +/// own 1970-3000 `gmtime()` range matters here too: `ts + offset` can be +/// pre-1970 or far-future even when `ts` itself would not be) into +/// **elephc's own** `_win_tz_tm_buf`, a glibc-layout `struct tm`: the 9 plain +/// `tm_sec..tm_isdst` ints at `+0..+36` (copied from msvcrt's result, except +/// `tm_isdst` which is overwritten with the bridge's DST flag), `tm_gmtoff` (an +/// `i64`, not part of MinGW's own `struct tm` — see its ``) at `+40`, and +/// `tm_zone` (`char*`) at `+48`, left `NULL` (no abbreviation lookup yet; the +/// `'T'` format specifier already treats a `NULL` `tm_zone` as "emit nothing", +/// so this degrades gracefully rather than misformatting). This buffer, not +/// msvcrt's own (which has no room for `tm_gmtoff`/`tm_zone`), is what the +/// `'Z'`/`'O'`/`'P'`/`'I'` `date()` format specifiers read at those offsets. +/// +/// ## Fallback path +/// When the slot is null or the zone is unresolvable, this asks msvcrt +/// `localtime()` for the host/TZ-rule decomposition, then copies its 36-byte +/// result into elephc's 56-byte extended buffer and derives `tm_gmtoff` by +/// interpreting the local fields through `_mkgmtime`. This is essential even in +/// fallback mode: returning msvcrt's shorter allocation directly would make the +/// shared date formatter read out of bounds at `tm_gmtoff`/`tm_zone`. If UCRT +/// rejects an out-of-range timestamp, the safe proleptic UTC decomposition is +/// returned instead of dereferencing `NULL`; this loses an unavailable local +/// offset but keeps pre-1970/post-3000 formatting memory-safe and deterministic. +pub(super) fn emit_shim_localtime(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_localtime"); + emitter.instruction("push rbp"); // preserve the caller frame pointer before the bridge lookup's stack-backed locals + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the saved timestamp/offset/isdst + // 80 bytes = a full, untouched 32-byte MSx64 shadow space at [rsp .. rsp+32) + // (msvcrt callees may scribble there even though this shim never passes a + // 5th+ stack arg) PLUS this frame's own 4 saved qwords at [rbp-32 .. rbp), + // i.e. [rsp+32 .. rsp+64) — a smaller frame here let msvcrt's gmtime() + // (whose calls into other MSVCRT internals do use shadow scratch) corrupt + // the saved offset/isdst mid-decomposition. + emitter.instruction("sub rsp, 80"); // shadow space + timestamp/offset/DST/abbreviation locals + emitter.instruction("mov rax, QWORD PTR [rdi]"); // ts = *timer + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save ts (survives every call below) + + emit_resolve_default_zone_name(emitter, "localtime"); // rdi = active default zone name (NUL-terminated) + emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // arg1 = ts + + emitter.instruction("mov r9, QWORD PTR [rip + _elephc_tz_offset_fn]"); // load the published elephc_tz_offset entry point (or null) + emitter.instruction("test r9, r9"); // no date/time call site published the bridge? + emitter.instruction("jz .Lsys_localtime_fallback"); // yes -> plain msvcrt localtime() + emitter.emit_native_bridge_call("r9", 2); // rax = packed (offset*2 + isdst), or the i64::MIN sentinel + emitter.instruction(&format!("mov r10, {}", TZ_OFFSET_SENTINEL_HEX)); // load the "unresolvable zone" sentinel + emitter.instruction("cmp rax, r10"); // did the bridge resolve the active zone? + emitter.instruction("je .Lsys_localtime_fallback"); // no -> plain msvcrt localtime() + + emitter.instruction("mov r10, rax"); // r10 = packed value + emitter.instruction("and r10, 1"); // r10 = isdst (packed value's low bit) + emitter.instruction("mov r11, rax"); // r11 = packed value + emitter.instruction("sub r11, r10"); // r11 = packed - isdst (always even) + emitter.instruction("sar r11, 1"); // r11 = offset seconds (exact: halves an even two's-complement value) + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // rax = ts + emitter.instruction("add rax, r11"); // rax = local_ts = ts + offset + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // stage local_ts so gmtime() can take its address + emitter.instruction("mov QWORD PTR [rbp - 24], r10"); // save isdst across the gmtime() call (r10 is MSx64-volatile) + emitter.instruction("mov QWORD PTR [rbp - 32], r11"); // save offset across the gmtime() call (r11 is MSx64-volatile) + + // -- resolve the exact transition abbreviation into its independent stable bridge cell -- + emit_resolve_default_zone_name(emitter, "localtime_abbr"); // rdi = active zone name + emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // rsi = original UTC timestamp + emitter.instruction("mov r9, QWORD PTR [rip + _elephc_tz_abbreviation_fn]"); // load optional abbreviation resolver + emitter.instruction("test r9, r9"); // resolver published with the offset bridge? + emitter.instruction("jz .Lsys_localtime_no_abbr"); // retain a null tm_zone when unavailable + emitter.emit_native_bridge_call("r9", 2); // rax = stable NUL-terminated transition abbreviation + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // preserve pointer across calendar decomposition + emitter.instruction("jmp .Lsys_localtime_abbr_ready"); // continue with the resolved pointer + emitter.label(".Lsys_localtime_no_abbr"); + emitter.instruction("mov QWORD PTR [rbp - 40], 0"); // no abbreviation bridge available + emitter.label(".Lsys_localtime_abbr_ready"); + + emitter.instruction("lea rcx, [rbp - 16]"); // &local_ts + emitter.instruction("call __rt_win_safe_gmtime"); // decomposes local_ts even outside UCRT's own 1970-3000 gmtime() range -> rax = its own static struct tm* (9 ints, no gmtoff/zone) + + emitter.instruction("lea r8, [rip + _win_tz_tm_buf]"); // r8 = elephc's own glibc-layout struct tm buffer + emitter.instruction("mov r11, QWORD PTR [rax + 0]"); // copy tm_sec/tm_min (msvcrt's decomposition of local_ts) + emitter.instruction("mov QWORD PTR [r8 + 0], r11"); // store tm_sec and tm_min in the glibc-layout buffer + emitter.instruction("mov r11, QWORD PTR [rax + 8]"); // copy tm_hour/tm_mday + emitter.instruction("mov QWORD PTR [r8 + 8], r11"); // store tm_hour and tm_mday in the glibc-layout buffer + emitter.instruction("mov r11, QWORD PTR [rax + 16]"); // copy tm_mon/tm_year + emitter.instruction("mov QWORD PTR [r8 + 16], r11"); // store tm_mon and tm_year in the glibc-layout buffer + emitter.instruction("mov r11, QWORD PTR [rax + 24]"); // copy tm_wday/tm_yday + emitter.instruction("mov QWORD PTR [r8 + 24], r11"); // store tm_wday and tm_yday in the glibc-layout buffer + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the bridge's isdst + emitter.instruction("mov DWORD PTR [r8 + 32], r10d"); // tm_isdst = bridge isdst (not msvcrt's gmtime()-always-0) + emitter.instruction("mov r11, QWORD PTR [rbp - 32]"); // reload the bridge offset + emitter.instruction("mov QWORD PTR [r8 + 40], r11"); // tm_gmtoff = offset (glibc extension field, absent from MinGW's own struct tm) + emitter.instruction("mov r11, QWORD PTR [rbp - 40]"); // stable abbreviation pointer owned by the bridge cell + emitter.instruction("mov QWORD PTR [r8 + 48], r11"); // tm_zone = exact transition abbreviation (CEST/CET/UTC/...) + emitter.instruction("mov rax, r8"); // return elephc's own buffer, not msvcrt's undersized one + emitter.instruction("add rsp, 80"); // release the frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return struct tm* in rax + + emitter.label(".Lsys_localtime_fallback"); + emitter.instruction("lea rcx, [rbp - 8]"); // &ts (equivalent to the caller's original time_t* — same value) + emitter.instruction("call localtime"); // msvcrt localtime(timer) -> struct tm* (its own static buffer) + emitter.instruction("test rax, rax"); // UCRT rejects timestamps outside its supported decomposition range + emitter.instruction("jz .Lsys_localtime_fallback_utc"); // rejected -> return a safe proleptic UTC decomposition + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // save msvcrt's 36-byte struct tm pointer + emitter.instruction("mov rcx, rax"); // pass the normalized local fields to _mkgmtime + emitter.instruction("call _mkgmtime"); // naive timestamp for the same local wall-clock fields + emitter.instruction("sub rax, QWORD PTR [rbp - 8]"); // derive local UTC offset = naive_local_ts - original_ts + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // preserve the derived offset while copying the source tm + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload msvcrt's source struct tm pointer + emitter.instruction("lea r8, [rip + _win_tz_tm_buf]"); // destination is always elephc's extended glibc-layout buffer + emitter.instruction("mov r11, QWORD PTR [rax + 0]"); // copy tm_sec/tm_min + emitter.instruction("mov QWORD PTR [r8 + 0], r11"); // store tm_sec/tm_min + emitter.instruction("mov r11, QWORD PTR [rax + 8]"); // copy tm_hour/tm_mday + emitter.instruction("mov QWORD PTR [r8 + 8], r11"); // store tm_hour/tm_mday + emitter.instruction("mov r11, QWORD PTR [rax + 16]"); // copy tm_mon/tm_year + emitter.instruction("mov QWORD PTR [r8 + 16], r11"); // store tm_mon/tm_year + emitter.instruction("mov r11, QWORD PTR [rax + 24]"); // copy tm_wday/tm_yday + emitter.instruction("mov QWORD PTR [r8 + 24], r11"); // store tm_wday/tm_yday + emitter.instruction("mov r11d, DWORD PTR [rax + 32]"); // load msvcrt tm_isdst + emitter.instruction("mov DWORD PTR [r8 + 32], r11d"); // preserve the fallback DST flag + emitter.instruction("mov r11, QWORD PTR [rbp - 32]"); // reload the derived UTC offset + emitter.instruction("mov QWORD PTR [r8 + 40], r11"); // synthesize the glibc-extension tm_gmtoff field + emitter.instruction("mov QWORD PTR [r8 + 48], 0"); // no reliable abbreviation pointer is available from msvcrt + emitter.instruction("mov rax, r8"); // return the extended buffer, never msvcrt's undersized one + emitter.instruction("add rsp, 80"); // release the frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return struct tm* in rax + + emitter.label(".Lsys_localtime_fallback_utc"); + emitter.instruction("lea rcx, [rbp - 8]"); // pass the original timestamp to the cycle-safe UTC decomposer + emitter.instruction("call __rt_win_safe_gmtime"); // obtain a non-null proleptic decomposition outside UCRT's localtime range + emitter.instruction("lea r8, [rip + _win_tz_tm_buf]"); // destination is elephc's extended struct tm buffer + emitter.instruction("mov r11, QWORD PTR [rax + 0]"); // copy tm_sec/tm_min + emitter.instruction("mov QWORD PTR [r8 + 0], r11"); // store tm_sec/tm_min + emitter.instruction("mov r11, QWORD PTR [rax + 8]"); // copy tm_hour/tm_mday + emitter.instruction("mov QWORD PTR [r8 + 8], r11"); // store tm_hour/tm_mday + emitter.instruction("mov r11, QWORD PTR [rax + 16]"); // copy tm_mon/tm_year + emitter.instruction("mov QWORD PTR [r8 + 16], r11"); // store tm_mon/tm_year + emitter.instruction("mov r11, QWORD PTR [rax + 24]"); // copy tm_wday/tm_yday + emitter.instruction("mov QWORD PTR [r8 + 24], r11"); // store tm_wday/tm_yday + emitter.instruction("mov DWORD PTR [r8 + 32], 0"); // UTC fallback never observes DST + emitter.instruction("mov QWORD PTR [r8 + 40], 0"); // UTC fallback offset is zero + emitter.instruction("mov QWORD PTR [r8 + 48], 0"); // no abbreviation pointer is synthesized + emitter.instruction("mov rax, r8"); // return the extended buffer + emitter.instruction("add rsp, 80"); // release the frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return struct tm* in rax + emitter.blank(); +} + +/// Resolves the active default timezone's identifier into `rdi` as a +/// NUL-terminated string, exactly as `__rt_date_default_timezone_get` +/// (`codegen_support/runtime/system/date_default_timezone.rs`) does: `_php_tz_env +/// + 3` (skipping the `"TZ="` prefix) when `_php_default_tz_len` is nonzero, +/// otherwise the literal `_php_tz_utc` ("UTC"). Both are NUL-terminated by their +/// writers (`__rt_date_default_timezone_set`/`__rt_tz_init_utc`), so the result is +/// a valid `elephc_tz_offset` `name` argument. `label_prefix` must be unique per +/// caller (this file has no per-function assembler scoping — see +/// `Emitter::label_global`'s Windows branch). +fn emit_resolve_default_zone_name(emitter: &mut Emitter, label_prefix: &str) { + emitter.instruction("mov rax, QWORD PTR [rip + _php_default_tz_len]"); // load the configured default-timezone length + emitter.instruction("test rax, rax"); // any zone explicitly set? + emitter.instruction(&format!("jnz .Lsys_{}_have_zone", label_prefix)); // yes -> use the configured identifier + emitter.instruction("lea rdi, [rip + _php_tz_utc]"); // none set -> default to "UTC" + emitter.instruction(&format!("jmp .Lsys_{}_zone_ready", label_prefix)); // bypass the configured-zone path after selecting UTC + emitter.label(&format!(".Lsys_{}_have_zone", label_prefix)); + emitter.instruction("lea rdi, [rip + _php_tz_env]"); // address of the configured "TZ=" env buffer + emitter.instruction("add rdi, 3"); // skip the "TZ=" prefix -> identifier pointer + emitter.label(&format!(".Lsys_{}_zone_ready", label_prefix)); +} + +/// Emits a shim that wraps `gmtime(const time_t*)`: SysV `rdi`=time_t* → +/// MSx64 `rcx`=time_t*, single argument, one-instruction shuffle, calling +/// [`emit_shim_win_safe_gmtime`]'s `__rt_win_safe_gmtime` (not bare msvcrt +/// `gmtime` — see its docblock: msvcrt/UCRT's `gmtime` returns `NULL` outside +/// 1970-3000, and `date()`/`gmdate()` must format arbitrary PHP-supplied +/// timestamps) and then copies the 9-int result into elephc's own +/// glibc-layout `struct tm` buffer (`_win_tz_tm_buf` — shared with +/// [`emit_shim_localtime`], which never runs concurrently with this shim +/// within one `date()`/`gmdate()` formatting call) so the `'Z'`/`'O'`/`'P'`/`'I'` +/// `date()` format specifiers find a real `tm_gmtoff`/`tm_zone` at offsets +/// `+40`/`+48` instead of reading whatever bytes happen to follow msvcrt's own +/// undersized 9-int buffer. +/// +/// ## Not part of the WF9 offset bridge +/// `gmtime` itself always decomposes in UTC regardless of `TZ`, so it needs no +/// offset resolution (unlike [`emit_shim_localtime`], which the bridge routes +/// through this same `__rt_win_safe_gmtime` helper internally, on its own +/// frame, when it decomposes `ts + offset`). The synthesized fields are +/// therefore fixed constants, not a bridge lookup: `tm_gmtoff = 0` and +/// `tm_isdst = 0` (UTC is always offset zero and never observes +/// daylight-saving time), and `tm_zone = NULL` (no abbreviation lookup; the +/// `'T'` format specifier already hardcodes `"GMT"` for `gmdate()` without +/// reading `tm_zone` at all — see +/// `codegen_support::runtime::system::date::linux_x86_64` — and treats a +/// `NULL` `tm_zone` as "emit nothing" on the `date()`/local path, matching +/// `emit_shim_localtime`'s own fallback convention). +pub(super) fn emit_shim_gmtime(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_gmtime"); + emitter.instruction("sub rsp, 40"); // shadow space + emitter.instruction("mov rcx, rdi"); // const time_t* + emitter.instruction("call __rt_win_safe_gmtime"); // decomposes timer even outside UCRT's own 1970-3000 gmtime() range -> rax = its own static struct tm* (9 ints, no gmtoff/zone) + emitter.instruction("lea r8, [rip + _win_tz_tm_buf]"); // r8 = elephc's own glibc-layout struct tm buffer + emitter.instruction("mov r9, QWORD PTR [rax + 0]"); // copy tm_sec/tm_min + emitter.instruction("mov QWORD PTR [r8 + 0], r9"); // store tm_sec and tm_min in the glibc-layout buffer + emitter.instruction("mov r9, QWORD PTR [rax + 8]"); // copy tm_hour/tm_mday + emitter.instruction("mov QWORD PTR [r8 + 8], r9"); // store tm_hour and tm_mday in the glibc-layout buffer + emitter.instruction("mov r9, QWORD PTR [rax + 16]"); // copy tm_mon/tm_year + emitter.instruction("mov QWORD PTR [r8 + 16], r9"); // store tm_mon and tm_year in the glibc-layout buffer + emitter.instruction("mov r9, QWORD PTR [rax + 24]"); // copy tm_wday/tm_yday + emitter.instruction("mov QWORD PTR [r8 + 24], r9"); // store tm_wday and tm_yday in the glibc-layout buffer + emitter.instruction("mov DWORD PTR [r8 + 32], 0"); // tm_isdst = 0 (UTC never observes DST) + emitter.instruction("mov QWORD PTR [r8 + 40], 0"); // tm_gmtoff = 0 (UTC is always offset zero) + emitter.instruction("mov QWORD PTR [r8 + 48], 0"); // tm_zone = NULL (no abbreviation lookup; see docblock) + emitter.instruction("mov rax, r8"); // return elephc's own buffer, not msvcrt's undersized one + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return struct tm* in rax + emitter.blank(); +} + +/// Emits a shim that wraps msvcrt `mktime(struct tm*)`: SysV `rdi`=struct tm* → +/// MSx64 `rcx`=struct tm*, converting the local wall-clock fields to a Unix +/// timestamp via the elephc-tz IANA offset bridge (WF9) when available, falling +/// back to plain msvcrt `mktime()`'s result otherwise. Return value (`time_t`) is +/// in `rax`. `mktime` also normalizes the `struct tm*` argument in place (e.g. +/// `tm_year`/`tm_wday`), which callers such as `__rt_mktime_shifted` rely on — +/// unaffected here, since msvcrt `mktime()` is always called first for exactly +/// that normalization side effect (see below), same as the plain-passthrough +/// shim this replaces. +/// +/// ## Bridge path +/// Always calls plain msvcrt `mktime(tm)` first — this normalizes the struct +/// (calendar overflow carrying is TZ-independent, so this step's normalization +/// is unaffected by which timezone ultimately supplies the offset) and its +/// return value is the fallback answer. Then, with the now-normalized fields, +/// calls msvcrt `_mkgmtime(tm)` to get `naive_utc_ts`: the timestamp that would +/// correspond to those exact `y/m/d/h/m/s` fields if read as literal UTC (this +/// re-normalizes identically, since the fields are already canonical — msvcrt +/// `_mkgmtime`, like `mktime`, only carries calendar overflow). If a call site +/// has published `elephc_tz_offset` into `_elephc_tz_offset_fn` +/// (`crate::codegen_support::tz_bridge`), resolves the active default timezone's +/// offset at `naive_utc_ts`, derives a candidate UTC instant, then resolves the +/// offset once more at that candidate. The second lookup is what makes ordinary +/// timestamps adjacent to a DST boundary use the transition rule at the actual +/// instant rather than at the naive wall-clock reading. It returns +/// `naive_utc_ts - corrected_offset` instead of the plain `mktime()` fallback. +/// +/// ## Fallback path +/// When the slot is null or the zone is unresolvable (an unknown name, or one +/// of the 11 legacy abbreviation-zones with no transition data), returns the +/// plain msvcrt `mktime()` result computed at entry — byte-identical to the +/// shim this replaces: offsets are then correct only for UTC or the host's +/// system timezone. +pub(super) fn emit_shim_mktime(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_mktime"); + emitter.instruction("push rbp"); // preserve the caller frame pointer before the bridge lookup's stack-backed locals + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the saved tm pointer/results + // See emit_shim_localtime's frame-sizing note: 80 bytes keeps a + // full, untouched 32-byte MSx64 shadow space below this frame's 4 saved + // qwords plus the UCRT pre-1970 shift marker. + emitter.instruction("sub rsp, 80"); // shadow space, saved values, and cycle marker (16-byte aligned) + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the struct tm pointer + + // -- UCRT accepts years 1970..3000 only; shift 1900..1969 by one identical Gregorian cycle -- + emitter.instruction("mov QWORD PTR [rbp - 40], 0"); // no internal 400-year shift by default + emitter.instruction("mov eax, DWORD PTR [rdi + 20]"); // tm_year, measured from 1900 + emitter.instruction("cmp eax, 70"); // already at least 1970? + emitter.instruction("jge .Lsys_mktime_ucrt_ready"); // UCRT accepts the original year + emitter.instruction("add DWORD PTR [rdi + 20], 400"); // preserve calendar shape while entering UCRT's range + emitter.instruction("mov QWORD PTR [rbp - 40], 1"); // remember to undo one cycle in fields/results + emitter.label(".Lsys_mktime_ucrt_ready"); + + emitter.instruction("mov rcx, rdi"); // struct tm* + emitter.instruction("call mktime"); // msvcrt mktime(tm) -> time_t; also normalizes tm in place (calendar overflow carry) + emitter.instruction("cmp QWORD PTR [rbp - 40], 0"); // internally shifted for UCRT? + emitter.instruction("je .Lsys_mktime_fallback_ready"); // retain the native result + emitter.instruction(&format!("movabs r10, {}", GREGORIAN_CYCLE_SECONDS)); // seconds in the added 400-year cycle + emitter.instruction("sub rax, r10"); // recover the original pre-1970 timestamp + emitter.label(".Lsys_mktime_fallback_ready"); + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // save the fallback/default result + + emitter.instruction("mov rcx, QWORD PTR [rbp - 8]"); // reload the (now-normalized) struct tm pointer + emitter.instruction("call _mkgmtime"); // msvcrt _mkgmtime(tm) -> naive-UTC reading of the same wall-clock fields + emitter.instruction("cmp QWORD PTR [rbp - 40], 0"); // internally shifted for UCRT? + emitter.instruction("je .Lsys_mktime_naive_ready"); // retain the native UTC reading + emitter.instruction(&format!("movabs r10, {}", GREGORIAN_CYCLE_SECONDS)); // seconds in the added cycle + emitter.instruction("sub rax, r10"); // recover the original naive UTC timestamp + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // normalized shifted struct tm + emitter.instruction("sub DWORD PTR [r10 + 20], 400"); // restore the caller-visible original tm_year cycle + emitter.label(".Lsys_mktime_naive_ready"); + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save naive_utc_ts + + emit_resolve_default_zone_name(emitter, "mktime"); // rdi = active default zone name (NUL-terminated) + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // arg1 = naive_utc_ts + + emitter.instruction("mov r9, QWORD PTR [rip + _elephc_tz_offset_fn]"); // load the published elephc_tz_offset entry point (or null) + emitter.instruction("test r9, r9"); // no date/time call site published the bridge? + emitter.instruction("jz .Lsys_mktime_done"); // yes -> keep the plain mktime() fallback result + emitter.emit_native_bridge_call("r9", 2); // rax = packed (offset*2 + isdst), or the i64::MIN sentinel + emitter.instruction(&format!("mov r10, {}", TZ_OFFSET_SENTINEL_HEX)); // load the "unresolvable zone" sentinel + emitter.instruction("cmp rax, r10"); // did the bridge resolve the active zone? + emitter.instruction("je .Lsys_mktime_done"); // no -> keep the plain mktime() fallback result + + emitter.instruction("mov r10, rax"); // r10 = packed value + emitter.instruction("and r10, 1"); // r10 = isdst (unused for mktime's own result; kept for symmetry with the lookup contract) + emitter.instruction("sub rax, r10"); // rax = packed - isdst (always even) + emitter.instruction("sar rax, 1"); // rax = offset seconds (exact: halves an even two's-complement value) + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // r11 = naive_utc_ts + emitter.instruction("sub r11, rax"); // r11 = first candidate UTC timestamp + emitter.instruction("mov QWORD PTR [rbp - 32], r11"); // preserve the first candidate across the transition-aware second lookup + + emitter.instruction("mov rsi, r11"); // arg1 = candidate UTC timestamp + emitter.instruction("mov r9, QWORD PTR [rip + _elephc_tz_offset_fn]"); // reload the bridge entry point clobbered by the first native call + emitter.emit_native_bridge_call("r9", 2); // resolve the zone offset at the actual candidate instant + emitter.instruction(&format!("mov r10, {}", TZ_OFFSET_SENTINEL_HEX)); // load the unresolvable-zone sentinel + emitter.instruction("cmp rax, r10"); // did the second lookup resolve? + emitter.instruction("je .Lsys_mktime_use_first_candidate"); // no -> retain the already valid first candidate + emitter.instruction("mov r10, rax"); // r10 = second packed value + emitter.instruction("and r10, 1"); // r10 = second lookup's DST bit + emitter.instruction("sub rax, r10"); // remove the DST bit from the packed offset + emitter.instruction("sar rax, 1"); // rax = corrected offset seconds + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // r11 = naive_utc_ts + emitter.instruction("sub r11, rax"); // r11 = transition-corrected UTC timestamp + emitter.instruction("mov QWORD PTR [rbp - 32], r11"); // replace the first candidate with the corrected one + emitter.label(".Lsys_mktime_use_first_candidate"); + emitter.instruction("mov r11, QWORD PTR [rbp - 32]"); // load the best bridge-derived candidate + emitter.instruction("mov QWORD PTR [rbp - 16], r11"); // overwrite the saved result with the bridge-corrected timestamp + + emitter.label(".Lsys_mktime_done"); + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // rax = final result (bridge-corrected, or the plain mktime() fallback) + emitter.instruction("add rsp, 80"); // release the frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return time_t in rax + emitter.blank(); +} + +/// Emits `__rt_sys_gettimeofday`, a custom-body shim synthesizing POSIX +/// `gettimeofday(struct timeval* tv, void* tz)` — msvcrt/UCRT export no such +/// symbol — via `GetSystemTimeAsFileTime`, modeled on [`emit_shim_clock_gettime`] +/// (which already converts a `FILETIME` to Unix-epoch seconds+remainder). SysV: +/// `rdi`=timeval*, `rsi`=tz (ignored, matching Linux's tz-ignored contract this +/// runtime already relies on). Unlike `clock_gettime`'s `timespec` (tv_nsec @ +8, +/// nanoseconds), `struct timeval` stores `tv_sec` @ +0 and `tv_usec` @ +8 in +/// MICROseconds — verified against this runtime's consumers: `time.rs`'s +/// `__rt_time` reads `tv_sec` from `[rsp]`/`[rdi]`, and `microtime.rs` reads both +/// `tv_sec` and `tv_usec` to build the fractional-second result. The FILETIME +/// remainder (100ns units) is divided by 10 a second time to convert it from +/// 100ns units to microseconds (vs. `clock_gettime`'s `imul rdx, 100` to convert +/// to nanoseconds). Always returns 0 (success) in `rax`, matching the success case +/// this runtime relies on (the return value is never checked by our callers). +/// +/// Writing `[rdi]`/`[rdi + 8]` *after* `call GetSystemTimeAsFileTime` is safe with no +/// spill because `rdi` (and `rsi`) are nonvolatile (callee-saved) in the Win64 ABI — +/// the Win32 call preserves them — so do not add an unnecessary rdi save/restore here. +pub(super) fn emit_shim_gettimeofday(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_gettimeofday"); + emitter.instruction("sub rsp, 40"); // shadow(32) + FILETIME(8) + emitter.instruction("lea rcx, [rsp + 32]"); // &filetime + emitter.instruction("call GetSystemTimeAsFileTime"); // get 100ns intervals since 1601 + emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // load FILETIME (64-bit) + emitter.instruction("mov r10, 116444736000000000"); // Unix epoch offset (100ns intervals from 1601 to 1970) + emitter.instruction("sub rax, r10"); // convert to Unix epoch (100ns intervals since 1970) + emitter.instruction("xor rdx, rdx"); // clear high 64 bits of dividend + emitter.instruction("mov r11, 10000000"); // divisor: 100ns intervals per second + emitter.instruction("div r11"); // RDX:RAX / r11 -> RAX = seconds, RDX = remainder (100ns units) + emitter.instruction("mov QWORD PTR [rdi], rax"); // store tv_sec @ +0 + emitter.instruction("mov rax, rdx"); // rax = remainder (100ns units, 0..9999999) + emitter.instruction("xor rdx, rdx"); // clear high 64 bits of dividend + emitter.instruction("mov r11, 10"); // divisor: 10 x 100ns = 1 microsecond + emitter.instruction("div r11"); // rax = remainder / 10 = microseconds + emitter.instruction("mov QWORD PTR [rdi + 8], rax"); // store tv_usec @ +8 (microseconds, not nanoseconds) + emitter.instruction("xor rax, rax"); // return 0 (success) + emitter.instruction("add rsp, 40"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits the `__rt_sys_getrusage` shim: converts Linux `getrusage(who, rusage*)` +/// (syscall 98) to Win32 `GetProcessTimes` and fills a Linux `struct rusage`. +/// +/// SysV: rdi = `who` (int: 0 = RUSAGE_SELF, -1 = RUSAGE_CHILDREN, 1 = RUSAGE_THREAD), +/// rsi = `struct rusage*` out. Only `RUSAGE_SELF` (who == 0) is populated: the user +/// and kernel FILETIMEs from `GetProcessTimes` are converted to `ru_utime`/`ru_stime` +/// (struct timeval: tv_sec @+0/+16, tv_usec @+8/+24). All other fields +/// (ru_maxrss..ru_nivcsw, offsets 32..144 = 14 qwords) are zeroed — Windows has no +/// clean RSS/page-fault equivalent and tests only check the time fields. For +/// `RUSAGE_CHILDREN`/`RUSAGE_THREAD` the whole struct is zeroed and 0 returned +/// (no child handle is available). Returns 0 on success, -1 on `GetProcessTimes` +/// failure. +/// +/// `struct rusage` (Linux x86_64) layout, hardcoded here with offsets: +/// - 0: ru_utime.tv_sec (i64), 8: ru_utime.tv_usec (i64) +/// - 16: ru_stime.tv_sec (i64), 24: ru_stime.tv_usec (i64) +/// - 32..144: ru_maxrss..ru_nivcsw (14 × i64), total struct = 144 bytes. +/// +/// FILETIME is a 64-bit count of 100ns intervals. tv_sec = ft / 10_000_000; +/// tv_usec = (ft % 10_000_000) / 10. +pub(super) fn emit_shim_getrusage(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_getrusage"); + // -- frame: shadow(32) + 5th-arg slot(8) + 4 FILETIMEs(32) + spill rusage(8) + + // spill who(8) = 88 bytes; 88 ≡ 8 mod 16 so rsp ≡ 0 at the Win32 call site -- + // [rsp+32] = lpUserTime ptr (MSx64 stack-arg slot) + // [rsp+40] = creation FILETIME, [rsp+48] = exit, [rsp+56] = kernel, [rsp+64] = user + // [rsp+72] = spill rusage ptr, [rsp+80] = spill who + emitter.instruction("sub rsp, 88"); // allocate frame (88 ≡ 8 mod 16) + emitter.instruction("mov QWORD PTR [rsp + 72], rsi"); // spill rusage out-pointer (rsi is volatile on MSx64) + emitter.instruction("mov DWORD PTR [rsp + 80], edi"); // spill who (edi — 32-bit int, SysV arg1) + emitter.instruction("cmp DWORD PTR [rsp + 80], 0"); // who == RUSAGE_SELF (0)? + emitter.instruction("jne .Lgetrusage_zero"); // → no child/thread handle: zero struct, return 0 + // -- GetProcessTimes(GetCurrentProcess()=(HANDLE)-1, &creation, &exit, &kernel, &user) -- + emitter.instruction("mov rcx, -1"); // hProcess = current-process pseudo-handle (HANDLE)-1 + emitter.instruction("lea rdx, [rsp + 40]"); // lpCreationTime = &creation FILETIME + emitter.instruction("lea r8, [rsp + 48]"); // lpExitTime = &exit FILETIME + emitter.instruction("lea r9, [rsp + 56]"); // lpKernelTime = &kernel FILETIME + emitter.instruction("lea rax, [rsp + 64]"); // lpUserTime = &user FILETIME + emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // 5th arg (lpUserTime) goes in the MSx64 stack-arg slot + emitter.instruction("call GetProcessTimes"); // fill the four FILETIMEs; eax = 0 on failure + emitter.instruction("test eax, eax"); // GetProcessTimes succeeded? + emitter.instruction("jz .Lgetrusage_fail"); // → failure: zero struct, return -1 + // -- convert kernel FILETIME → ru_stime (tv_sec @+16, tv_usec @+24) -- + emitter.instruction("mov r11, QWORD PTR [rsp + 72]"); // rusage pointer (reloaded; r11 stays across no further calls) + emitter.instruction("mov rax, QWORD PTR [rsp + 56]"); // kernel FILETIME (64-bit 100ns count) + emitter.instruction("xor rdx, rdx"); // clear high half of dividend before unsigned div + emitter.instruction("mov ecx, 10000000"); // divisor: 10_000_000 (100ns units per second) + emitter.instruction("div rcx"); // rax = tv_sec, rdx = remainder (100ns units) + emitter.instruction("mov QWORD PTR [r11 + 16], rax"); // ru_stime.tv_sec = kernel_seconds + emitter.instruction("mov rax, rdx"); // remainder (100ns units within the last second) + emitter.instruction("xor rdx, rdx"); // clear high half of dividend before unsigned div + emitter.instruction("mov ecx, 10"); // divisor: 10 (100ns units per microsecond) + emitter.instruction("div rcx"); // rax = tv_usec + emitter.instruction("mov QWORD PTR [r11 + 24], rax"); // ru_stime.tv_usec = kernel_useconds + // -- convert user FILETIME → ru_utime (tv_sec @+0, tv_usec @+8) -- + emitter.instruction("mov rax, QWORD PTR [rsp + 64]"); // user FILETIME (64-bit 100ns count) + emitter.instruction("xor rdx, rdx"); // clear high half of dividend before unsigned div + emitter.instruction("mov ecx, 10000000"); // divisor: 10_000_000 (100ns units per second) + emitter.instruction("div rcx"); // rax = tv_sec, rdx = remainder (100ns units) + emitter.instruction("mov QWORD PTR [r11 + 0], rax"); // ru_utime.tv_sec = user_seconds + emitter.instruction("mov rax, rdx"); // remainder (100ns units within the last second) + emitter.instruction("xor rdx, rdx"); // clear high half of dividend before unsigned div + emitter.instruction("mov ecx, 10"); // divisor: 10 (100ns units per microsecond) + emitter.instruction("div rcx"); // rax = tv_usec + emitter.instruction("mov QWORD PTR [r11 + 8], rax"); // ru_utime.tv_usec = user_useconds + // -- zero ru_maxrss..ru_nivcsw (offsets 32..144, 14 qwords) -- + emitter.instruction("mov rdi, r11"); // rep stosq destination = rusage base + emitter.instruction("add rdi, 32"); // point at ru_maxrss (offset 32) + emitter.instruction("xor rax, rax"); // fill value = 0 + emitter.instruction("mov rcx, 14"); // 14 qwords (112 bytes) cover ru_maxrss..ru_nivcsw + emitter.instruction("rep stosq"); // zero the remaining rusage fields + emitter.instruction("xor eax, eax"); // return 0 (success) + emitter.instruction("add rsp, 88"); // restore stack + emitter.instruction("ret"); // return + // -- who != RUSAGE_SELF: zero the whole struct (18 qwords = 144 bytes) and return 0 -- + emitter.label(".Lgetrusage_zero"); + emitter.instruction("mov rdi, QWORD PTR [rsp + 72]"); // rusage pointer + emitter.instruction("xor rax, rax"); // fill value = 0 + emitter.instruction("mov rcx, 18"); // 18 qwords (144 bytes) = full struct rusage + emitter.instruction("rep stosq"); // zero the entire struct + emitter.instruction("xor eax, eax"); // return 0 (success, but no times available) + emitter.instruction("add rsp, 88"); // restore stack + emitter.instruction("ret"); // return + // -- GetProcessTimes failed: zero the whole struct and return -1 -- + emitter.label(".Lgetrusage_fail"); + emitter.instruction("mov rdi, QWORD PTR [rsp + 72]"); // rusage pointer + emitter.instruction("xor rax, rax"); // fill value = 0 + emitter.instruction("mov rcx, 18"); // 18 qwords (144 bytes) = full struct rusage + emitter.instruction("rep stosq"); // zero the entire struct + emitter.instruction("mov eax, -1"); // return -1 (failure) + emitter.instruction("add rsp, 88"); // restore stack + emitter.instruction("ret"); // return + emitter.blank(); +} + +/// Emits a clock_getres shim — writes a 1ns resolution (best-effort) into the +/// output `struct timespec *res` (rsi), NULL-guarded since POSIX permits +/// `res == NULL`. `struct timespec` is `{ tv_sec @+0, tv_nsec @+8 }`, both +/// 8-byte fields on x64. +pub(super) fn emit_shim_clock_getres(emitter: &mut Emitter) { + emitter.label_global("__rt_sys_clock_getres"); + emitter.instruction("test rsi, rsi"); // res == NULL? (POSIX permits a NULL res) + emitter.instruction("jz .Lclock_getres_done"); // NULL -> skip the write, just report success + emitter.instruction("mov QWORD PTR [rsi], 0"); // tv_sec = 0 + emitter.instruction("mov QWORD PTR [rsi + 8], 1"); // tv_nsec = 1 (1ns best-effort, matches docblock) + emitter.label(".Lclock_getres_done"); + emitter.instruction("xor rax, rax"); // return 0 (success) + emitter.instruction("ret"); // return + emitter.blank(); +} diff --git a/src/codegen_support/runtime/win32/tests.rs b/src/codegen_support/runtime/win32/tests.rs new file mode 100644 index 0000000000..6e98a91209 --- /dev/null +++ b/src/codegen_support/runtime/win32/tests.rs @@ -0,0 +1,2814 @@ +//! Unit tests for the Win32 shim emitters (mirrors the assembly assertions +//! used throughout the `shims_*` submodules). + +use super::*; +use crate::codegen::platform::Target; + +/// Verifies x86_64 stat consumers read the portable layout written by Windows shims. +#[test] +fn test_windows_stat_consumers_use_portable_layout_offsets() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + crate::codegen_support::runtime::io::emit_stat(&mut emitter); + crate::codegen_support::runtime::io::emit_stat_ext(&mut emitter); + crate::codegen_support::runtime::io::emit_stat_array(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains("mov r9d, DWORD PTR [rsp + 16]")); + assert!(asm.contains("mov eax, DWORD PTR [rsp + 16]")); + assert!(asm.contains("mov eax, DWORD PTR [rbp - 136]")); + assert!(!asm.contains("mov r9d, DWORD PTR [rsp + 24]")); +} + +/// Verifies that Win32 shims emit the expected symbols for windows-x86_64. +#[test] +fn test_win32_shims_emit_expected_symbols() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_win32_shims(&mut emitter, RuntimeFeatures::all()); + let asm = emitter.output(); + for sym in [ + "__rt_sys_write", + "__rt_sys_read", + "__rt_sys_exit", + "__rt_sys_close", + "__rt_sys_mmap", + "__rt_sys_open", + ] { + assert!( + asm.contains(&format!(".globl {}\n", sym)), + "Win32 shim missing global symbol {}", + sym + ); + } +} + +/// Verifies that Win32 imports are declared. +#[test] +fn test_win32_imports_declared() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_win32_shims(&mut emitter, RuntimeFeatures::all()); + let asm = emitter.output(); + assert!(asm.contains(".extern GetStdHandle")); + assert!(asm.contains(".extern WriteFile")); + assert!(asm.contains(".extern ExitProcess")); + assert!(asm.contains(".extern HeapAlloc")); + assert!(asm.contains(".extern WSASend")); + assert!(asm.contains(".extern WSARecv")); + assert!(asm.contains(".extern PeekNamedPipe")); + assert!(asm.contains(".extern GetTempFileNameW")); + assert!(asm.contains(".extern getprotobyname")); + assert!(asm.contains(".extern getprotobynumber")); + assert!(asm.contains(".extern getservbyname")); + assert!(asm.contains(".extern getservbyport")); +} + +/// Verifies Windows netdb helpers use Winsock shims rather than Unix database files. +#[test] +fn test_win32_netdb_runtime_helpers_call_winsock() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + crate::codegen_support::runtime::io::emit_getprotobyname(&mut emitter); + crate::codegen_support::runtime::io::emit_getprotobynumber(&mut emitter); + crate::codegen_support::runtime::io::emit_getservbyname(&mut emitter); + crate::codegen_support::runtime::io::emit_getservbyport(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains("call __rt_sys_getprotobyname")); + assert!(asm.contains("call __rt_sys_getprotobynumber")); + assert!(asm.contains("call __rt_sys_getservbyname")); + assert!(asm.contains("call __rt_sys_getservbyport")); + assert!(asm.contains("movsx rax, WORD PTR [rax + 16]")); + assert!(asm.contains("test rcx, rcx")); + assert!(asm.contains("jz __rt_gsbn_windows_missing")); + assert!(asm.contains("movzx eax, WORD PTR [rax + 24]")); + assert!(asm.contains("call __rt_str_persist")); + assert!(asm.matches("sub rsp, 8").count() >= 2); + assert!(asm.matches("add rsp, 8").count() >= 4); + assert!(!asm.contains("__rt_protoent_load")); + assert!(!asm.contains("__rt_servent_load")); +} + +/// Verifies that fd-to-HANDLE conversion consistently uses the CRT descriptor table. +#[test] +fn test_fd_to_handle_emitted() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_fd_to_handle(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("call _get_osfhandle")); + assert!(!asm.contains("call GetStdHandle")); +} + +/// Verifies opaque Windows stream descriptors are mapped to bounded slots before +/// EOF/filter state is indexed, and that close releases the mapping and state. +#[test] +fn test_stream_state_uses_bounded_opaque_handle_slots() { + let mut registry = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_win_stream_slot_registry(&mut registry); + emit_win_stream_slot_clear(&mut registry); + emit_win_stream_mark_timed_out(&mut registry); + let registry_asm = registry.output(); + assert!(registry_asm.contains(".globl __rt_win_stream_slot\n")); + assert!(registry_asm.contains("cmp ecx, 256")); + assert!(registry_asm.contains("_win_stream_slot_handles")); + assert!(registry_asm.contains("_win_stream_slot_used")); + assert!(registry_asm.contains("_win_stream_timed_out")); + assert!(registry_asm.contains("_stream_read_filters")); + assert!(registry_asm.contains("_stream_write_filters")); + + let mut close = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_close(&mut close); + let close_asm = close.output(); + assert_eq!(close_asm.matches("call __rt_win_stream_slot_clear").count(), 2); + + let mut feof = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + crate::codegen_support::runtime::io::emit_feof(&mut feof); + let feof_asm = feof.output(); + assert!(feof_asm.contains("call __rt_win_stream_slot")); + assert!(feof_asm.contains("mov rdi, rax")); + assert!(feof_asm.contains("BYTE PTR [r10 + rdi]")); + + let mut consumers = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + crate::codegen_support::runtime::io::emit_fread(&mut consumers); + crate::codegen_support::runtime::io::emit_fgets(&mut consumers); + crate::codegen_support::runtime::io::emit_fwrite(&mut consumers); + crate::codegen_support::runtime::io::emit_fopen(&mut consumers); + crate::codegen_support::runtime::io::emit_stream_get_line(&mut consumers); + crate::codegen_support::runtime::io::emit_stream_get_meta_data(&mut consumers); + crate::codegen_support::runtime::io::emit_streams_ext(&mut consumers); + crate::codegen_support::runtime::io::emit_stream_filter_attach_user(&mut consumers); + crate::codegen_support::runtime::io::emit_apply_user_stream_filter(&mut consumers); + crate::codegen_support::runtime::io::emit_user_filter_release_fd(&mut consumers); + let consumers_asm = consumers.output(); + assert!(consumers_asm.matches("call __rt_win_stream_slot").count() >= 12); + assert!(consumers_asm.contains("_stream_read_filters")); + assert!(consumers_asm.contains("_stream_write_filters")); + assert!(consumers_asm.contains("_eof_flags")); + assert!(consumers_asm.contains("_win_stream_timed_out")); + assert!(consumers_asm.contains("__rt_win_stream_mark_timed_out")); + assert!(consumers_asm.contains("cmp rax, -3")); + assert!(consumers_asm.contains("jne __rt_fread_would_block_x86")); + assert!(consumers_asm.contains("mov rax, -1")); + + let mut timeout = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + crate::codegen_support::runtime::io::emit_stream_set_timeout(&mut timeout); + let timeout_asm = timeout.output(); + assert!(timeout_asm.contains("mov rdx, 20")); + assert!(timeout_asm.contains("mov rdx, 21")); + assert_eq!(timeout_asm.matches("call __rt_sys_setsockopt").count(), 2); + assert!(timeout_asm.contains("_win_stream_timed_out")); +} + +/// Verifies optional write-filter handle tables also use compact slots on Windows. +#[test] +fn test_optional_filter_handles_use_windows_stream_slots() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + crate::codegen_support::stream_filters::zlib::emit_x86_64( + &mut emitter, + "zlib_write", + "zlib_close", + "zlib_skip", + 6, + ); + crate::codegen_support::stream_filters::bzip2::emit_compress_x86_64( + &mut emitter, + "bz2_write", + "bz2_close", + "bz2_skip", + 9, + 0, + ); + crate::codegen_support::stream_filters::iconv_write::emit_iconv_write_attach_with_labels( + &mut emitter, + "_iconv_from", + "_iconv_to", + |prefix| format!("{}_slot_test", prefix), + ); + let asm = emitter.output(); + assert!(asm.matches("call __rt_win_stream_slot").count() >= 12); + assert!(asm.contains("_zstream_handles")); + assert!(asm.contains("_bzstream_handles")); + assert!(asm.contains("_iconv_handles")); + assert!(!asm.contains("QWORD PTR [r9 + rdi*8]")); +} + +/// Verifies that file close releases the CRT descriptor rather than bypassing its table. +#[test] +fn test_close_shim_uses_crt_descriptor_contract() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_close(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("call _close")); + assert!(!asm.contains("call CloseHandle")); +} + +/// Verifies that the main wrapper shuffles MSx64 args to SysV and initializes Winsock. +#[test] +fn test_main_wrapper_shuffles_args() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_main_wrapper(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("call __rt_winsock_init"), "main wrapper must call winsock init"); + assert!(asm.contains("call __elephc_main")); + // argc/argv are spilled to the stack across the winsock init call (rcx/rdx + // are volatile on MSx64 and clobbered by WSAStartup) and reloaded into the + // SysV arg registers rdi/rsi before __elephc_main. + assert!(asm.contains("mov QWORD PTR [rsp + 0], rcx"), "argc must be spilled before the init call"); + assert!(asm.contains("mov QWORD PTR [rsp + 8], rdx"), "argv must be spilled before the init call"); + assert!(asm.contains("mov rdi, QWORD PTR [rsp + 0]"), "argc must be reloaded into rdi after the init call"); + assert!(asm.contains("mov rsi, QWORD PTR [rsp + 8]"), "argv must be reloaded into rsi after the init call"); + // The winsock init call must occur before __elephc_main so sockets work. + let init_pos = asm.find("call __rt_winsock_init"); + let main_pos = asm.find("call __elephc_main"); + assert!(init_pos.is_some() && main_pos.is_some() && init_pos < main_pos); +} + +/// Verifies that newly added shims for previously-missing syscalls are emitted. +#[test] +fn test_new_shims_emitted() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_win32_shims(&mut emitter, RuntimeFeatures::all()); + let asm = emitter.output(); + for sym in [ + "__rt_sys_lseek", + "__rt_sys_socketpair", + "__rt_sys_statfs", + "__rt_sys_pselect6", + "__rt_sys_sendmsg", + "__rt_sys_recvmsg", + "__rt_sys_getdents", + "__rt_sys_creat", + "__rt_sys_clock_getres", + "__rt_sys_newfstatat", + "__rt_sys_dup", + "__rt_sys_dup2", + ] { + assert!( + asm.contains(&format!(".globl {}\n", sym)), + "Win32 shim missing global symbol {}", + sym + ); + } +} + +/// Verifies that fcntl preserves F_GETFL state and applies F_SETFL through FIONBIO. +#[test] +fn test_fcntl_tracks_flags_and_applies_socket_nonblocking_mode() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_fcntl(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("__rt_win_fd_status_find")); + assert!(asm.contains("__rt_win_fd_status_upsert")); + assert!(asm.contains("mov edx, 0x8004667e")); + assert!(asm.contains("call ioctlsocket")); +} + +/// Verifies Windows ownership shims reject even existing paths like php-src. +#[test] +fn test_libc_chown_shims_return_failure_without_path_probe() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_libc_chown_unsupported(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains(".globl __rt_sys_libc_chown\n"), "chown shim label missing"); + assert!(asm.contains(".globl __rt_sys_libc_lchown\n"), "lchown shim label missing"); + assert_eq!( + asm.matches("mov rax, -1").count(), + 2, + "both chown and lchown must return failure" + ); + assert!( + !asm.contains("__rt_sys_access"), + "Windows chown/lchown must not probe or mutate the path" + ); +} + +/// Verifies argv bootstrap delegates quote/backslash parsing to shell32 and converts every +/// resulting Unicode argument into independently owned strict UTF-8 storage. +#[test] +fn test_init_argv_uses_native_unicode_quoting_and_balanced_ownership() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_sys_init_argv(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_init_argv"); + assert!(section.contains("call GetCommandLineW")); + assert!(section.contains("call CommandLineToArgvW")); + assert!(!section.contains("call GetCommandLineA")); + assert_eq!(section.matches("call __rt_win_utf16_to_utf8").count(), 2); + assert!(section.contains("call LocalFree")); + assert!(section.contains(".Linit_argv_w_cleanup")); + assert!(section.matches("call __rt_heap_free").count() >= 3); + assert!(section.contains("mov QWORD PTR [rip + _global_argc], rax")); + assert!(section.contains("mov QWORD PTR [rip + _global_argv], rax")); +} + +/// Verifies process argv cleanup releases each converted string before its +/// pointer table and clears the globals so exit may invoke it defensively. +#[test] +fn test_free_argv_releases_runtime_heap_ownership_and_is_idempotent() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_sys_free_argv(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_free_argv"); + assert_eq!(section.matches("call __rt_heap_free").count(), 2); + assert!(section.contains("mov rax, QWORD PTR [r10 + rcx * 8]")); + assert!(section.contains("mov QWORD PTR [rip + _global_argc], 0")); + assert!(section.contains("mov QWORD PTR [rip + _global_argv], 0")); +} + +/// Verifies that open shim handles PHP fopen creation flags, including exclusive create. +#[test] +fn test_open_shim_handles_flags() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_open(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("0x40"), "O_CREAT check missing"); + assert!(asm.contains("0x80"), "O_EXCL check missing"); + assert!(asm.contains("0x200"), "O_TRUNC check missing"); + assert!(asm.contains("0x400"), "O_APPEND check missing"); +} + +/// Verifies Windows `fopen` parses PHP's full base-mode and suffix-mode surface. +#[test] +fn test_windows_fopen_mode_parser_emits_full_php_modes() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + crate::codegen_support::runtime::io::emit_fopen(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("__rt_fopen_check_x_x86")); + assert!(asm.contains("__rt_fopen_check_c_x86")); + assert!(asm.contains("__rt_fopen_modifier_next_x86")); + assert!(asm.contains("__rt_fopen_modifier_t_x86")); + assert!(asm.contains("0x80"), "x mode must retain O_EXCL"); + assert!(asm.contains("0x4000"), "t mode must retain _O_TEXT"); +} + +/// Verifies `open` converts Unicode/extended paths once and stages the seven-argument +/// `CreateFileW` call in non-overlapping MSx64 stack slots. +#[test] +fn test_open_shim_uses_wide_path_and_windows_call_layout() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_open(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_open"); + assert!(section.contains("sub rsp, 88")); + assert_eq!(section.matches("call __rt_win_utf8_to_utf16").count(), 1); + assert!(section.contains("mov rcx, QWORD PTR [rsp + 56]")); + assert!(section.contains("mov QWORD PTR [rsp + 32], r10")); + assert!(section.contains("mov QWORD PTR [rsp + 40], 0")); + assert!(section.contains("mov QWORD PTR [rsp + 48], 0")); + assert!(section.contains("call CreateFileW")); + assert!(section.contains("call _open_osfhandle")); + assert!(section.contains("cmp eax, -1")); + assert!(section.contains("mov edx, 0x8000")); + assert!(section.contains("test DWORD PTR [rsp + 64], 0x4000")); + assert!(section.contains("mov edx, 0x4000")); + assert!(!section.contains("call CreateFileA")); + assert!(section.contains("call __rt_heap_free")); + assert!(section.contains("call GetLastError")); + assert!(section.contains("__rt_win32_last_error")); + assert!(section.contains("__rt_errno")); +} + +/// Verifies path-based stat uses the Unicode attribute and handle APIs while preserving +/// the documented Windows structure offsets and releasing its converted long path. +#[test] +fn test_stat_shim_uses_wide_metadata_and_windows_layouts() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_stat(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_stat"); + assert!(section.contains("sub rsp, 168")); + assert!(section.contains("add rsp, 168")); + assert_eq!(section.matches("call __rt_win_utf8_to_utf16").count(), 1); + assert!(section.contains("call GetFileAttributesExW")); + assert!(!section.contains("call GetFileAttributesExA")); + assert!(section.contains("mov eax, DWORD PTR [rsp + 64]")); + assert!(section.contains("mov edx, DWORD PTR [rsp + 60]")); + assert!(section.contains("lea rdi, [rsp + 88]")); + assert!(section.contains("mov eax, DWORD PTR [rsp + 132]")); + assert!(section.contains("mov ecx, DWORD PTR [rsp + 136]")); + assert!(section.contains("call CreateFileW")); + assert!(!section.contains("call CreateFileA")); + assert!(section.contains("call GetFileInformationByHandle")); + assert!(section.contains("call __rt_heap_free")); + assert!(section.contains("call GetLastError")); + assert!(section.contains("__rt_errno")); +} + +/// Verifies that getrandom caps BCrypt's 32-bit length and reports the partial count. +#[test] +fn test_getrandom_returns_count() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_getrandom(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("BCryptGenRandom")); + assert!(asm.contains("mov rax, 4294967295")); + assert!(asm.contains("cmova rsi, rax")); + assert!(asm.contains("mov QWORD PTR [rsp + 32], rsi")); + assert!(asm.contains(".Lgetrandom_fail")); +} + +/// Verifies that kill shim uses OpenProcess+TerminateProcess for SIGKILL. +#[test] +fn test_kill_uses_terminate_process() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_kill(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("OpenProcess")); + assert!(asm.contains("TerminateProcess")); +} + +/// Verifies that writev shim saves iov pointer on stack (not in rsi). +#[test] +fn test_writev_saves_iov_ptr() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_writev(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("[rsp + 8]"), "iov pointer should be saved on stack"); +} + +/// Verifies that _dup and _dup2 are in the imports list. +#[test] +fn test_dup_imports_declared() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_win32_shims(&mut emitter, RuntimeFeatures::all()); + let asm = emitter.output(); + assert!(asm.contains(".extern _dup")); + assert!(asm.contains(".extern _dup2")); +} + +/// Verifies that the 6 previously-missing __rt_sys_* shims are now emitted. +#[test] +fn test_missing_sys_shims_now_emitted() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_win32_shims(&mut emitter, RuntimeFeatures::all()); + let asm = emitter.output(); + for sym in [ + "__rt_sys_link", + "__rt_sys_symlink", + "__rt_sys_readlink", + "__rt_sys_chown", + "__rt_sys_fchown", + "__rt_sys_lchown", + ] { + assert!( + asm.contains(&format!(".globl {}\n", sym)), + "Missing __rt_sys_* shim: {}", + sym + ); + } +} + +/// Verifies realtime conversion keeps its divisor out of `rdx`, while monotonic +/// requests use the performance counter and write through the real second +/// `clock_gettime` argument (`rsi`), not the clock id in `rdi`. +#[test] +fn test_clock_gettime_realtime_and_monotonic_contracts() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_clock_gettime(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("xor rdx, rdx"), "RDX must be cleared before div"); + assert!(asm.contains("mov r11, 10000000"), "Divisor should be in r11"); + assert!(asm.contains("div r11"), "Should divide by r11, not rdx"); + assert!(!asm.contains("div rdx"), "Must NOT divide by rdx (crash bug)"); + assert!(asm.contains("cmp edi, 1"), "CLOCK_MONOTONIC must be recognized"); + assert!(asm.contains("call QueryPerformanceCounter")); + assert!(asm.contains("call QueryPerformanceFrequency")); + assert!(asm.contains("mov QWORD PTR [rsi], rax")); + assert!(asm.contains("mov QWORD PTR [rsi + 8], rax")); + assert!( + !asm.contains("mov QWORD PTR [rdi], rax"), + "clock_id must never be mistaken for the timespec pointer" + ); +} + +/// Verifies every `localtime` exit returns elephc's extended struct-tm buffer, +/// including the no-bridge/unresolvable-zone fallback and UCRT's out-of-range +/// null result, so date formatters can safely read `tm_gmtoff` at offset 40. +#[test] +fn test_localtime_fallback_synthesizes_extended_tm_layout() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_localtime(&mut emitter); + let asm = emitter.output(); + let fallback = asm + .split(".Lsys_localtime_fallback:") + .nth(1) + .expect("localtime fallback label"); + assert!(fallback.contains("call localtime")); + assert!(fallback.contains("test rax, rax")); + assert!(fallback.contains("call _mkgmtime")); + assert!(fallback.contains("lea r8, [rip + _win_tz_tm_buf]")); + assert!(fallback.contains("mov QWORD PTR [r8 + 40], r11")); + assert!(fallback.contains("call __rt_win_safe_gmtime")); + assert_eq!( + fallback.matches("mov rax, r8").count(), + 2, + "both the msvcrt and out-of-range fallback paths must return the extended buffer" + ); +} + +/// Verifies localtime publishes the bridge-owned transition abbreviation as `tm_zone`. +#[test] +fn test_localtime_resolves_transition_abbreviation() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_localtime(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("_elephc_tz_abbreviation_fn")); + assert!(asm.contains("mov QWORD PTR [rbp - 40], rax")); + assert!(asm.contains("mov QWORD PTR [r8 + 48], r11")); +} + +/// Verifies Windows `mktime` resolves the IANA offset again at the first UTC +/// candidate, preventing the naive wall-clock lookup from selecting the wrong +/// side of a nearby DST transition. +#[test] +fn test_mktime_rechecks_offset_at_candidate_instant() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_mktime(&mut emitter); + let asm = emitter.output(); + assert_eq!( + asm.matches("call r11").count(), + 2, + "mktime must perform an initial and candidate-instant bridge lookup" + ); + assert!(asm.contains("mov QWORD PTR [rbp - 32], r11")); + assert!(asm.contains(".Lsys_mktime_use_first_candidate")); +} + +/// Verifies the Win32 mktime boundary shifts UCRT-rejected 1900..1969 years by one cycle. +#[test] +fn test_mktime_shifts_pre_1970_for_ucrt() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_mktime(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("cmp eax, 70")); + assert!(asm.contains("add DWORD PTR [rdi + 20], 400")); + assert!(asm.contains("sub DWORD PTR [r10 + 20], 400")); + assert!(asm.contains(&format!("movabs r10, {}", 146_097_i64 * 86_400))); +} + +/// Verifies that utimensat sets all seven `CreateFileW` arguments. +#[test] +fn test_utimensat_has_all_createfile_args() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_c_symbols(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("utimensat")); + assert!(asm.contains("call __rt_win_utf8_to_utf16")); + assert!(asm.contains("call CreateFileW")); + // arg 5: dwCreationDisposition at [rsp+32] + assert!(asm.contains("[rsp + 32], 3")); + // arg 6: dwFlagsAndAttributes at [rsp+40] (FILE_FLAG_BACKUP_SEMANTICS, so directories open too) + assert!(asm.contains("[rsp + 40], 0x2000000")); + // arg 7: hTemplateFile at [rsp+48] + assert!(asm.contains("[rsp + 48], 0")); + // SetFileTime actually receives the requested atime/mtime FILETIMEs, not NULL + assert!(asm.contains("call SetFileTime")); + assert!(asm.contains("lea r8, [rsp + 72]")); + assert!(asm.contains("lea r9, [rsp + 80]")); +} + +/// Verifies the rename shim translates the Win32 `MoveFileExW` BOOL result +/// (nonzero = success) to the POSIX convention (`0` = success, `-1` = +/// failure) that `__rt_rename` tests with `cmp eax, 0` — without it a +/// successful rename would be reported as a failure and vice versa. +#[test] +fn test_rename_translates_bool_to_posix() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_rename(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("call MoveFileExW"), "rename must overwrite via MoveFileExW"); + assert_eq!(asm.matches("call __rt_win_utf8_to_utf16").count(), 2); + assert!(asm.contains("mov r8d, 3"), "MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED"); + assert!(asm.contains("test eax, eax"), "must test the Win32 BOOL result"); + assert!(asm.contains(".Lrename_fail"), "must branch to the POSIX failure path"); + assert!(asm.contains("xor eax, eax"), "success translates to POSIX 0"); + assert!(asm.contains("mov rax, -1"), "failure translates to POSIX -1"); +} + +/// Verifies that symlink shim retries with ALLOW_UNPRIVILEGED_CREATE. +#[test] +fn test_symlink_unprivileged_retry() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_c_symbols(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("mov r8, 2"), "Should set ALLOW_UNPRIVILEGED_CREATE"); + assert!(asm.contains(".Lsymlink_ok")); + assert!(asm.contains(".Lsymlink_fail")); +} + +/// Verifies that readlink keeps CreateFile stack arguments separate from owned paths. +#[test] +fn test_readlink_clean_stack_layout() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_c_symbols(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "readlink"); + assert!(section.contains("mov QWORD PTR [rsp + 88], rsi")); + assert!(section.contains("mov QWORD PTR [rsp + 96], rdx")); + assert!(section.contains("mov QWORD PTR [rsp + 40], 0x2200000")); + assert!(!asm.contains("Wait"), "No leftover debugging comments"); +} + +/// Verifies that all shims use 16-byte aligned stack frames (sub rsp, 40 not 32). +/// +/// A bare `sub rsp, 32` directly off a shim's own entry would misalign the stack +/// (32 is a multiple of 16, but such shims are entered at rsp ≡ 8 mod 16, so they +/// need `sub rsp, K` with K ≡ 8 mod 16 — 40 or 56 — to re-align before the call). +/// Two patterns legitimately use `sub rsp, 32` anyway, both re-aligned by a +/// *different* instruction first: `__rt_sys_exit`, which executes `and rsp, -16` +/// to force 16-byte alignment (safe because the shim never returns) before its +/// `sub rsp, 32` ExitProcess shadow space; and `Emitter::emit_native_bridge_call` +/// (a ≤4-arg native-bridge call — see its own unit tests), whose `sub rsp, 32` +/// MSx64 shadow space follows a `push rbp; mov rbp, rsp; sub rsp, ` frame in the calling shim (e.g. WF9's `__rt_sys_localtime`/ +/// `__rt_sys_mktime`, which call the elephc-tz bridge through it) — that +/// established rsp ≡ 0 mod 16 already, which `sub rsp, 32` preserves, and its +/// immediately-preceding emitted line is always `mov r11, ` (the fn-ptr +/// relocation, skipped only when the pointer is already in r11). Permit +/// `sub rsp, 32` only when the immediately-preceding emitted line is +/// `and rsp, -16` or starts with `mov r11, `; reject it anywhere else. +#[test] +fn test_stack_alignment_16_bytes() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_win32_shims(&mut emitter, RuntimeFeatures::all()); + let asm = emitter.output(); + let lines: Vec<&str> = asm.lines().collect(); + for (i, line) in lines.iter().enumerate() { + if line.trim().starts_with("sub rsp, 32") { + let prev = if i > 0 { lines[i - 1].trim() } else { "" }; + assert!( + prev.starts_with("and rsp, -16") || prev.starts_with("mov r11, "), + "Only the force-aligned exit shim or a native-bridge call's fn-ptr \ + relocation (`Emitter::emit_native_bridge_call`) may use sub rsp, 32; \ + found a bare sub rsp, 32 (misaligned off a shim's own rsp ≡ 8 mod 16 \ + entry) not preceded by `and rsp, -16` or `mov r11, ...`. Use 40 or 56." + ); + } + } +} + +/// Verifies that sendto passes all 6 arguments. +#[test] +fn test_sendto_passes_6_args() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_socket_shims(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_sendto"); + assert!(section.contains("mov QWORD PTR [rsp + 32], rax"), "sendto must stage translated/native dest_addr as the fifth MSx64 argument"); + assert!(section.contains("mov QWORD PTR [rsp + 40], rax"), "sendto must stage translated/native addrlen as the sixth MSx64 argument"); + assert!(section.contains("mov r9, QWORD PTR [rsp + 56]"), "sendto must preserve flags before registry lookups"); +} + +/// Verifies that Windows rejects AF_UNIX before consulting the native provider. +#[test] +fn test_af_unix_is_rejected_for_php_windows_parity() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_socket_shims(&mut emitter); + let asm = emitter.output(); + let socket = shim_section(&asm, "__rt_sys_socket"); + assert!( + socket.contains("cmp edi, 1") + && socket.contains("je .Lsocket_unix_unsupported") + && socket.contains("mov DWORD PTR [rip + __rt_errno], 97"), + "AF_UNIX must fail with EAFNOSUPPORT before Winsock dispatch" + ); +} + +/// Verifies that Windows socketpair uses php-src's AF_INET loopback emulation. +#[test] +fn test_socketpair_emulates_php_windows_af_inet_surface() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_socketpair(&mut emitter); + let asm = emitter.output(); + let socketpair = shim_section(&asm, "__rt_sys_socketpair"); + assert!( + socketpair.contains("cmp edi, 2") + && socketpair.contains("call socket") + && socketpair.contains("call bind") + && socketpair.contains("call connect") + && socketpair.contains("call accept"), + "Windows socketpair must create an AF_INET loopback pair" + ); + assert!( + socketpair.contains("mov DWORD PTR [rip + __rt_errno], 92"), + "non-AF_INET families must fail with ENOPROTOOPT" + ); +} + +/// Verifies that recvfrom passes all 6 arguments. +#[test] +fn test_recvfrom_passes_6_args() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_socket_shims(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_recvfrom"); + assert!(section.contains("mov QWORD PTR [rsp + 32], rax"), "recvfrom must stage native/temporary src_addr as the fifth MSx64 argument"); + assert!(section.contains("mov QWORD PTR [rsp + 40], rax"), "recvfrom must stage native/temporary addrlen as the sixth MSx64 argument"); + assert!(section.contains("call __rt_win_unix_write_sockaddr"), "recvfrom must restore AF_UNIX source addresses after loopback reception"); +} + +/// Verifies that setsockopt passes all 5 arguments. +#[test] +fn test_setsockopt_passes_5_args() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_setsockopt(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("[rsp + 32], r8"), "setsockopt: optlen should be at [rsp+32]"); + assert!(asm.contains("mov r9, r10"), "setsockopt: optval should go to r9"); +} + +/// Verifies that getsockopt passes all 5 arguments. +#[test] +fn test_getsockopt_passes_5_args() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_getsockopt(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("[rsp + 32], r8"), "getsockopt: &optlen should be at [rsp+32]"); + assert!(asm.contains("mov r9, r10"), "getsockopt: optval should go to r9"); +} + +/// F5 regression: `setsockopt` translates POSIX `SOL_SOCKET` (1) to Winsock's `0xFFFF` +/// before calling Winsock `setsockopt`, gated on the ORIGINAL (pre-translation) level. +#[test] +fn test_setsockopt_translates_sol_socket_level() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_setsockopt(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_setsockopt"); + assert!( + section.contains("cmp rsi, 1"), + "must test the original POSIX SOL_SOCKET level" + ); + assert!( + section.contains("mov rsi, 0xffff"), + "POSIX SOL_SOCKET(1) must translate to Winsock 0xFFFF" + ); +} + +/// F5 regression: `setsockopt` translates POSIX `SO_RCVTIMEO`(20)/`SO_SNDTIMEO`(21) to +/// Winsock's `0x1006`/`0x1005`, and other `SOL_SOCKET` optnames (`SO_REUSEADDR`, +/// `SO_KEEPALIVE`, `SO_BROADCAST`) to their Winsock numeric values. +#[test] +fn test_setsockopt_translates_sol_socket_optnames() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_setsockopt(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_setsockopt"); + assert!( + section.contains("mov rdx, 0x1006"), + "SO_RCVTIMEO(20) must translate to Winsock 0x1006" + ); + assert!( + section.contains("mov rdx, 0x1005"), + "SO_SNDTIMEO(21) must translate to Winsock 0x1005" + ); + assert!( + section.contains(" mov rdx, 4\n"), + "SO_REUSEADDR(2) must translate to Winsock 4" + ); + assert!( + section.contains(" mov rdx, 8\n"), + "SO_KEEPALIVE(9) must translate to Winsock 8" + ); + assert!( + section.contains("mov rdx, 0x20"), + "SO_BROADCAST(6) must translate to Winsock 0x20" + ); +} + +/// F5 regression: an unmapped `SOL_SOCKET` optname (e.g. POSIX `SO_LINGER`=13, which +/// elephc never emits and the shim has no Winsock translation for) must pass through +/// UNCHANGED rather than being silently dropped or corrupted — the shim falls through to +/// the unconditional "pass through unchanged" jump instead of matching a known `je` case. +#[test] +fn test_setsockopt_unknown_optname_falls_through_unchanged() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_setsockopt(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_setsockopt"); + assert!( + !section.contains("cmp rdx, 13"), + "no known translation exists for SO_LINGER(13): it must not be special-cased" + ); + assert!( + section.contains("jmp .Lsetsockopt_after_optname"), + "unmatched SOL_SOCKET optnames must fall through to the untranslated path" + ); +} + +/// F5 regression: php-src's Windows trap — `SO_RCVTIMEO`/`SO_SNDTIMEO` take a plain +/// `DWORD` millisecond count on Winsock, not a `struct timeval`. The shim must convert +/// the POSIX timeval at `[r10]` (`tv_sec`@+0, `tv_usec`@+8) into milliseconds and +/// redirect `optval`/`optlen` (`optlen=4`) at that DWORD before calling Winsock. +#[test] +fn test_setsockopt_converts_timeval_to_ms_for_timeout_options() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_setsockopt(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_setsockopt"); + assert!( + section.contains("mov rax, QWORD PTR [r10 + 8]"), + "must read tv_usec from the POSIX timeval" + ); + assert!( + section.contains("mov rax, QWORD PTR [r10]"), + "must read tv_sec from the POSIX timeval" + ); + assert!( + section.contains("idiv r11"), + "must divide tv_usec by 1000 to get whole milliseconds" + ); + assert!( + section.contains("imul rax, rax, 1000"), + "must scale tv_sec to milliseconds" + ); + assert!( + section.contains("add rax, r11"), + "ms = tv_sec*1000 + tv_usec/1000" + ); + assert!( + section.contains("mov DWORD PTR [rsp + 48], eax"), + "must stash the computed ms as a 32-bit DWORD" + ); + assert!( + section.contains("lea r10, [rsp + 48]"), + "optval must be redirected to the ms DWORD" + ); + assert!( + section.contains(" mov r8, 4\n"), + "optlen must be overridden to sizeof(DWORD) = 4 for the timeout payload" + ); +} + +/// Register-clobber regression: the ms-conversion's `cqo`/`idiv` pair destructively +/// overwrites `rdx` (`cqo` sign-extends `rax` into `rdx:rax`; `idiv` then leaves the +/// division remainder in `rdx`), but `rdx` is exactly where the Winsock optname +/// (`0x1006`/`0x1005`) was staged just before entering the timeout payload. A prior +/// version of this shim let `idiv` silently replace the optname with +/// `tv_usec % 1000`, so Winsock received a garbage optname on every SO_RCVTIMEO/ +/// SO_SNDTIMEO call. The shim must stash `rdx` to the unused `[rsp + 40]` pad slot +/// BEFORE `cqo` and restore it AFTER `idiv` completes, strictly in that order. +#[test] +fn test_setsockopt_preserves_optname_across_timeout_division() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_setsockopt(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_setsockopt"); + assert!( + section.contains("mov QWORD PTR [rsp + 40], rdx"), + "optname must be stashed to the unused [rsp + 40] pad slot before the division" + ); + assert!( + section.contains("mov rdx, QWORD PTR [rsp + 40]"), + "optname must be restored from [rsp + 40] after the division" + ); + let stash_pos = section + .find("mov QWORD PTR [rsp + 40], rdx") + .expect("optname stash missing"); + let cqo_pos = section.find("cqo").expect("cqo missing"); + let idiv_pos = section.find("idiv r11").expect("idiv missing"); + let restore_pos = section + .find("mov rdx, QWORD PTR [rsp + 40]") + .expect("optname restore missing"); + assert!( + stash_pos < cqo_pos && cqo_pos < idiv_pos && idiv_pos < restore_pos, + "optname must be stashed before cqo and restored only after idiv completes \ + (stash={stash_pos}, cqo={cqo_pos}, idiv={idiv_pos}, restore={restore_pos})" + ); + // The restored optname must actually reach the Winsock call's 3rd argument (r8), + // not just sit in rdx unused — `mov r8, rdx` in `.Lsetsockopt_after_optname` must + // come after the restore, otherwise the fix stashes/restores a value nothing reads. + let stage_optname_pos = section + .rfind("mov r8, rdx") + .expect("optname staging into r8 missing"); + assert!( + restore_pos < stage_optname_pos, + "restored optname must flow to `mov r8, rdx` before the Winsock call" + ); +} + +/// F5 regression: non-`SOL_SOCKET` levels (e.g. POSIX `IPPROTO_TCP`=6, used by +/// `TCP_NODELAY`) must skip optname/level translation entirely — both are already +/// numerically identical to their Winsock counterparts. +#[test] +fn test_setsockopt_non_sol_socket_level_skips_translation() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_setsockopt(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_setsockopt"); + assert!( + section.contains("cmp rsi, 1") && section.contains("je .Lsetsockopt_sol_socket"), + "optname translation must be gated on level == POSIX SOL_SOCKET" + ); + assert!( + section.contains("jne .Lsetsockopt_after_level"), + "level translation must also be gated so non-SOL_SOCKET levels stay unchanged" + ); +} + +/// F5 regression: elephc's IPv6 stream-server emits `IPV6_V6ONLY` at level +/// `IPPROTO_IPV6`=41 with the Linux optname 26; Winsock uses 27 (level 41 is identical +/// on both). The shim must gate on the original level==41 and translate optname 26 → 27. +#[test] +fn test_setsockopt_translates_ipv6_v6only_optname() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_setsockopt(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_setsockopt"); + assert!( + section.contains("cmp rsi, 41") && section.contains("je .Lsetsockopt_ipv6"), + "IPPROTO_IPV6 optname translation must be gated on the original level == 41" + ); + assert!( + section.contains(" mov rdx, 27\n"), + "IPV6_V6ONLY(26) must translate to Winsock 27" + ); +} + +/// F5 regression: Windows has no `SO_REUSEPORT`; forwarding POSIX optname 15 raw makes +/// Winsock reject it with `WSAENOPROTOOPT`. The shim maps `SO_REUSEPORT`(15) → Winsock +/// `SO_REUSEADDR`(4), the substitution php-src uses on Windows for address-reuse. +#[test] +fn test_setsockopt_maps_reuseport_to_reuseaddr() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_setsockopt(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_setsockopt"); + assert!( + section.contains("cmp rdx, 15") && section.contains("je .Lsetsockopt_is_reuseport"), + "must recognize POSIX SO_REUSEPORT (optname 15)" + ); + // The SO_REUSEPORT target block sets rdx to Winsock SO_REUSEADDR (4). Assert against + // the instruction immediately under the reuseport label — the bare `mov rdx, 4` is + // shared with the separate SO_REUSEADDR(2) target, so the label anchor is what scopes + // this to the reuseport case (Rust `//` comments never reach the emitted asm). + assert!( + section.contains(".Lsetsockopt_is_reuseport:\n mov rdx, 4\n"), + "SO_REUSEPORT(15) must map to Winsock SO_REUSEADDR(4)" + ); +} + +/// Returns the assembly slice for the shim labeled `label`, from its `.globl` +/// declaration up to (but excluding) the next `.globl` declaration — used to scope +/// `cdqe` presence/absence assertions to a single shim's body instead of the whole +/// (possibly multi-shim) emitter output. +fn shim_section<'a>(asm: &'a str, label: &str) -> &'a str { + let marker = format!(".globl {}\n", label); + let start = asm + .find(&marker) + .unwrap_or_else(|| panic!("shim {} not found in emitted asm", label)); + let after = &asm[start + marker.len()..]; + match after.find(".globl ") { + Some(next) => &after[..next], + None => after, + } +} + +/// Sign-extension (Classe 3) regression suite: every `__rt_sys_*` shim that returns a +/// 32-bit int status (0/`SOCKET_ERROR`=-1) and has a sign-testing consumer must `cdqe` +/// before returning, so a -1 failure reads as a 64-bit negative instead of +/// `0x00000000_FFFFFFFF` (positive, a missed failure). `socket`/`accept` return a +/// 64-bit `SOCKET` handle and must NOT `cdqe` (it would corrupt a handle with bit 31 +/// set). See `emit_shim_socket_shims`'s docblock for the full rationale. +mod sign_extension { + use super::*; + + /// Verifies that all six int-status socket shims — `bind`, `listen`, `connect`, + /// `shutdown`, `getsockname`, `getpeername` — are emitted as dedicated blocks AFTER + /// the shared `shims` loop (which now contains ONLY `socket`/`accept`), each ending + /// with `cdqe` before `ret`, matching the connect gabarit this class of fix is + /// copied from. `accept` is the last shared-loop entry, so every dedicated block + /// must appear strictly after it — proving they are no longer loop-driven. + #[test] + fn test_int_status_socket_shims_are_dedicated_cdqe_blocks() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_socket_shims(&mut emitter); + let asm = emitter.output(); + + let accept_pos = asm + .find(".globl __rt_sys_accept\n") + .expect("accept shim missing"); + for label in [ + "__rt_sys_bind", + "__rt_sys_listen", + "__rt_sys_connect", + "__rt_sys_shutdown", + "__rt_sys_getsockname", + "__rt_sys_getpeername", + ] { + let pos = asm + .find(&format!(".globl {}\n", label)) + .unwrap_or_else(|| panic!("{} shim missing", label)); + assert!( + pos > accept_pos, + "{} must be emitted as a dedicated block after the shared loop (found before accept)", + label + ); + let section = shim_section(&asm, label); + assert!( + section.contains("cdqe"), + "{} must sign-extend its Winsock int return with cdqe", + label + ); + } + } + + /// Verifies the shared `shims` loop is now reduced to ONLY `socket`/`accept` — the + /// two 64-bit SOCKET-handle returns that must NOT be sign-extended (`cdqe` would + /// corrupt a handle with bit 31 set). Every other former loop entry + /// (shutdown/getsockname/getpeername) was extracted into a dedicated cdqe block. + #[test] + fn test_socket_and_accept_have_no_cdqe() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_socket_shims(&mut emitter); + let asm = emitter.output(); + for label in ["__rt_sys_socket", "__rt_sys_accept"] { + let section = shim_section(&asm, label); + assert!( + !section.contains("cdqe"), + "{} returns a 64-bit SOCKET handle and must NOT cdqe", + label + ); + } + } + + /// Verifies the three int-status shims extracted out of the shared loop in the + /// correction loop — `shutdown`, `getsockname`, `getpeername` — each sign-extend + /// their Winsock int return. These had ZERO cdqe coverage before this test: + /// shutdown's consumer is stream_socket_shutdown.rs:47 (`test rax,rax; js`), + /// getsockname/getpeername share stream_socket_get_name.rs:310 (`cmp rax,0; jl`). + #[test] + fn test_shutdown_getsockname_getpeername_sign_extend() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_socket_shims(&mut emitter); + let asm = emitter.output(); + for label in [ + "__rt_sys_shutdown", + "__rt_sys_getsockname", + "__rt_sys_getpeername", + ] { + let section = shim_section(&asm, label); + assert!(section.contains("cdqe"), "{} must cdqe before returning", label); + } + } + + /// Verifies sendmsg/recvmsg translate Linux iovecs to Win64 WSABUFs and + /// route synchronous scatter/gather I/O through Winsock. + #[test] + fn test_sendmsg_recvmsg_use_winsock_scatter_gather() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_sendmsg(&mut emitter); + emit_shim_recvmsg(&mut emitter); + let asm = emitter.output(); + for (label, native_call) in [ + ("__rt_sys_sendmsg", "call WSASend"), + ("__rt_sys_recvmsg", "call WSARecv"), + ] { + let section = shim_section(&asm, label); + assert!(section.contains(native_call), "{label} must use {native_call}"); + assert!(section.contains("QWORD PTR [r10 + 16]")); + assert!(section.contains("QWORD PTR [rsi + 24]")); + assert!(section.contains("shl r9, 4")); + assert!(section.contains("QWORD PTR [r10 + r9 + 8]")); + assert!(section.contains("DWORD PTR [rax + r9], r11d")); + assert!(section.contains("QWORD PTR [rax + r9 + 8], r11")); + assert!(section.contains("call WSAGetLastError")); + assert!(section.contains("call __rt_win32_errno_from_code")); + assert!(section.contains("call __rt_heap_free")); + } + let recv = shim_section(&asm, "__rt_sys_recvmsg"); + assert!(recv.contains("DWORD PTR [r10 + 48], eax")); + } + + /// Verifies `sendto`/`recvfrom` sign-extend their Winsock byte-count-or-error + /// return (consumers at stream_socket_sendto.rs:415 / stream_socket_recvfrom.rs:204 + /// sign-test with `cmp rax,0; jl`). + #[test] + fn test_sendto_recvfrom_sign_extend() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_socket_shims(&mut emitter); + let asm = emitter.output(); + for label in ["__rt_sys_sendto", "__rt_sys_recvfrom"] { + let section = shim_section(&asm, label); + assert!(section.contains("cdqe"), "{} must cdqe before returning", label); + } + } + + /// Verifies `setsockopt` sign-extends its Winsock int-status return (consumer + /// stream_set_timeout.rs:75 sign-tests with `cmp rax,0; jl`). + #[test] + fn test_setsockopt_sign_extends() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_setsockopt(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_setsockopt"); + assert!(section.contains("cdqe"), "setsockopt must cdqe before returning"); + } + + /// Verifies latent `getsockopt` still reports a correctly signed status and errno. + #[test] + fn test_getsockopt_sign_extends_and_captures_errno() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_getsockopt(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_getsockopt"); + assert!(section.contains("cdqe")); + assert!(section.contains("call __rt_wsa_capture_errno")); + } + + /// Verifies `_dup`/`_dup2` sign-extend their msvcrt int-status return (-1 on + /// failure). + #[test] + fn test_dup_dup2_sign_extend() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_dup_shims(&mut emitter); + let asm = emitter.output(); + for label in ["__rt_sys_dup", "__rt_sys_dup2"] { + let section = shim_section(&asm, label); + assert!(section.contains("cdqe"), "{} must cdqe before returning", label); + } + } + + /// Verifies `ioctl` (→ ioctlsocket) sign-extends its int-status return; reached + /// from `stream_set_blocking()` via the fcntl F_SETFL/FIONBIO delegation, which + /// sign-tests with `test rax,rax; js` at stream_set_blocking.rs:94/114. + #[test] + fn test_ioctl_shim_sign_extends() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_ioctl(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_ioctl"); + assert!(section.contains("cdqe"), "ioctl shim must cdqe before returning"); + } + + /// Verifies `lseek` uses the 64-bit `SetFilePointerEx` contract and maps + /// native failures instead of truncating offsets through a DWORD return. + #[test] + fn test_lseek_shim_preserves_full_width_positions() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_lseek(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_lseek"); + assert!(section.contains("call SetFilePointerEx")); + assert!(section.contains("lea r8, [rsp + 48]")); + assert!(section.contains("mov rax, QWORD PTR [rsp + 48]")); + assert!(section.contains("call __rt_win32_errno_from_code")); + assert!(!section.contains("call SetFilePointer\n")); + } + + /// Verifies `pselect6` tests Winsock's 32-bit SOCKET_ERROR sentinel in EAX + /// and widens the result before saving it for the shared return path. + #[test] + fn test_pselect6_success_return_sign_extends() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_pselect6(&mut emitter); + let asm = emitter.output(); + let select_section = shim_section(&asm, "__rt_sys_pselect6"); + assert!(select_section.contains("call select\n movsxd rax, eax")); + assert!(select_section.contains("cmp eax, -1")); + assert!(!select_section.contains("cmp rax, -1")); + let reload_pos = asm + .find("mov rax, QWORD PTR [rsp + 72]") + .expect("pselect6 must reload the saved select result from [rsp+72]"); + let cdqe_pos = asm + .find("cdqe") + .expect("pselect6 success return must contain cdqe"); + let ret_pos = asm[cdqe_pos..] + .find("ret") + .map(|p| p + cdqe_pos) + .expect("no ret found after cdqe"); + assert!( + reload_pos < cdqe_pos && cdqe_pos < ret_pos, + "cdqe must sit between the [rsp+72] reload ({}) and the following ret ({}), \ + found at {}", + reload_pos, + ret_pos, + cdqe_pos + ); + } +} + +/// Verifies that flock supplies a real OVERLAPPED to both whole-file lock APIs. +#[test] +fn test_flock_stack_alignment_for_6_args() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_c_symbols(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("flock")); + let section = shim_section(&asm, "flock"); + assert!(section.contains("sub rsp, 104")); + assert!(section.contains("movdqu XMMWORD PTR [rsp + 48], xmm0")); + assert!(section.contains("mov QWORD PTR [rsp + 40], rax")); + assert!(section.contains("test QWORD PTR [rsp + 88], 8")); + assert!(section.contains("call __rt_win32_errno_from_code")); +} + +/// Verifies WriteFile shim uses the MSx64-correct 5th-arg layout: lpOverlapped=NULL +/// at [rsp+32] (arg5) and the &bytesWritten output pointer at [rsp+40], never +/// colliding on the arg5 slot. +#[test] +fn test_write_shim_overlapped_and_output_offsets() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_write(&mut emitter); + let asm = emitter.output(); + assert!( + asm.contains("mov QWORD PTR [rsp + 32], 0"), + "lpOverlapped NULL must be at the arg5 slot [rsp+32]" + ); + assert!( + asm.contains("lea r9, [rsp + 40]"), + "&bytesWritten (arg4) must point at [rsp+40], off the arg5 slot" + ); + assert!( + asm.contains("mov eax, DWORD PTR [rsp + 40]"), + "bytesWritten must be read back as a 4-byte DWORD from [rsp+40]" + ); + assert!( + !asm.contains("lea r9, [rsp + 32]"), + "output pointer must not alias the arg5 (lpOverlapped) slot" + ); + // `len` must be spilled to the stack and reloaded across the intervening + // `call __rt_fd_to_handle` — r8 is volatile in MSx64 and may be clobbered. + assert!( + asm.contains("mov QWORD PTR [rsp + 48], rdx"), + "len must be spilled to [rsp+48] before the handle-conversion call" + ); + assert!( + asm.contains("mov r8, QWORD PTR [rsp + 48]"), + "len must be reloaded from [rsp+48] into r8 after the handle-conversion call" + ); + assert!( + !asm.contains("mov r8, rdx"), + "len must not be parked in volatile r8 across the call (regression guard)" + ); + assert!(asm.contains("test eax, eax")); + assert!(asm.contains("jz .Lsys_write_file_fail")); + assert!(asm.contains("call GetLastError")); + assert!(asm.contains("call __rt_win32_errno_from_code")); + assert!(asm.contains("mov DWORD PTR [rip + __rt_errno], eax")); + assert!(asm.contains("mov rax, -1")); +} + +/// Verifies ReadFile shim uses the MSx64-correct 5th-arg layout: lpOverlapped=NULL +/// at [rsp+32] (arg5), &bytesRead at [rsp+48], and the preserved length at [rsp+72]. +#[test] +fn test_read_shim_overlapped_and_output_offsets() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_read(&mut emitter); + let asm = emitter.output(); + assert!( + asm.contains("mov QWORD PTR [rsp + 32], 0"), + "lpOverlapped NULL must be at the arg5 slot [rsp+32]" + ); + assert!( + asm.contains("lea r9, [rsp + 48]"), + "&bytesRead (arg4) must point at [rsp+48], off the arg5 slot" + ); + assert!( + asm.contains("mov eax, DWORD PTR [rsp + 48]"), + "bytesRead must be read back as a 4-byte DWORD from [rsp+48]" + ); + assert!( + !asm.contains("lea r9, [rsp + 32]"), + "output pointer must not alias the arg5 (lpOverlapped) slot" + ); + // `len` must be spilled to the stack and reloaded across the intervening + // `call __rt_fd_to_handle` — r8 is volatile in MSx64 and may be clobbered. + assert!( + asm.contains("mov QWORD PTR [rsp + 72], rdx"), + "len must be spilled to [rsp+72] before the handle-conversion call" + ); + assert!( + asm.contains("mov r8, QWORD PTR [rsp + 72]"), + "len must be reloaded from [rsp+72] into r8 after the handle-conversion call" + ); + assert!( + !asm.contains("mov r8, rdx"), + "len must not be parked in volatile r8 across the call (regression guard)" + ); +} + +/// Verifies the lseek shim preserves both full-width distance and whence across +/// the intervening handle conversion before loading the MS x64 arguments. +#[test] +fn test_lseek_shim_spills_distance_and_whence_across_call() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_lseek(&mut emitter); + let asm = emitter.output(); + assert!( + asm.contains("mov QWORD PTR [rsp + 32], rsi"), + "distance must be spilled to [rsp+32] before the handle-conversion call" + ); + assert!( + asm.contains("mov QWORD PTR [rsp + 40], rdx"), + "whence must be spilled to [rsp+40] before the handle-conversion call" + ); + assert!( + asm.contains("mov rdx, QWORD PTR [rsp + 32]"), + "distance must be reloaded as the full-width LARGE_INTEGER argument" + ); + assert!( + asm.contains("mov r9d, DWORD PTR [rsp + 40]"), + "whence must be reloaded into the fourth MS x64 argument" + ); +} + +/// Verifies exact readlink spills the reparse buffer, handle, and byte result across calls. +#[test] +fn test_readlink_shim_spills_handle_and_length_across_calls() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_c_symbols(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "readlink"); + assert!(section.contains("mov QWORD PTR [rsp + 72], rax")); + assert!(section.contains("mov QWORD PTR [rsp + 80], rax")); + assert!(section.contains("mov rcx, QWORD PTR [rsp + 80]")); + assert!(section.contains("mov QWORD PTR [rsp + 136], rax")); + assert!( + !asm.contains("mov rcx, r10"), + "readlink handle must not survive a call in volatile r10 (regression guard)" + ); +} + +/// Verifies the remaining filesystem path shims use strict wide APIs, normalize extended +/// drive/UNC prefixes on returned paths, and balance owned conversions with errno paths. +#[test] +fn test_final_path_family_uses_unicode_and_extended_prefix_normalization() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_c_symbols(&mut emitter); + let asm = emitter.output(); + for (label, api) in [ + ("symlink", "CreateSymbolicLinkW"), + ("link", "CreateHardLinkW"), + ("readlink", "DeviceIoControl"), + ("utimensat", "CreateFileW"), + ("realpath", "GetFinalPathNameByHandleW"), + ] { + let section = shim_section(&asm, label); + assert!(section.contains("call __rt_win_utf8_to_utf16"), "{label}"); + assert!(section.contains(&format!("call {api}")), "{label}"); + assert!(section.contains("__rt_errno"), "{label}"); + assert!(section.contains("call __rt_heap_free"), "{label}"); + } + let readlink = shim_section(&asm, "readlink"); + assert!(readlink.contains("0x2200000")); + assert!(readlink.contains("0x900A8")); + assert!(readlink.contains("0xA000000C")); + assert!(readlink.contains("0xA0000003")); + assert!(readlink.contains("WORD PTR [r10 + 14]")); + assert!(readlink.contains("WORD PTR [r10 + 10]")); + assert!(readlink.contains("cmp DWORD PTR [rsp + 104], 20")); + assert!(readlink.contains("cmp DWORD PTR [rsp + 104], 16")); + assert!(readlink.contains("lea r8, [rdx + rax]")); + assert!(readlink.contains("cmova rax, rcx")); + assert!(readlink.contains("rep movsb")); + assert!(readlink.contains("0x005c003f005c005c")); + assert!(readlink.contains("0x005c003f003f005c")); + assert!(readlink.contains("0x005c0043004e0055")); + assert!(readlink.contains("call __rt_win_utf16_to_utf8")); + let realpath = shim_section(&asm, "realpath"); + assert!(realpath.contains("call CreateFileW")); + assert!(realpath.contains("mov r8d, 7")); + assert!(realpath.contains("mov QWORD PTR [rsp + 32], 3")); + assert!(realpath.contains("mov QWORD PTR [rsp + 40], 0x2000080")); + assert!(realpath.contains("mov QWORD PTR [rsp + 48], 0")); + assert!(realpath.contains("call GetFinalPathNameByHandleW")); + assert!(realpath.contains("call CloseHandle")); + assert!(!realpath.contains("call GetFullPathNameW")); + assert!(realpath.contains("0x005c003f005c005c")); + assert!(realpath.contains("0x005c0043004e0055")); + assert!(realpath.contains("call __rt_win_utf16_to_utf8")); + let lstat_probe = shim_section(&asm, "__rt_lstat_is_symlink"); + assert!(lstat_probe.contains("mov QWORD PTR [rsp + 40], 0x2200000")); + assert!(lstat_probe.contains("call DeviceIoControl")); + assert!(lstat_probe.contains("cmp DWORD PTR [rax], 0xA000000C")); + for ansi in [ + "CreateSymbolicLinkA", + "CreateHardLinkA", + "GetFinalPathNameByHandleA", + "GetFullPathNameA", + ] { + assert!(!asm.contains(&format!("call {ansi}")), "ANSI path call remains: {ansi}"); + } +} + +/// Verifies `lstat` preserves php-src's tri-state reparse semantics and link modes. +#[test] +fn test_lstat_reparse_ioctl_failure_is_not_silently_followed() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_c_symbols(&mut emitter); + let asm = emitter.output(); + let probe = shim_section(&asm, "__rt_lstat_is_symlink"); + assert!(probe.contains("call GetFileAttributesW")); + assert!(probe.contains("test eax, 0x400")); + assert!(probe.contains("jz .Llstat_probe_cleanup_wide_false")); + assert!(probe.contains("call DeviceIoControl")); + assert!(probe.contains("call GetLastError")); + assert!(probe.contains(".Llstat_probe_cleanup_error:")); + assert!(probe.contains("mov rax, -1")); + assert!(probe.contains("call __rt_win32_errno_from_code")); + + let lstat = shim_section(&asm, "lstat"); + assert!(lstat.contains("js .Llstat_probe_fail")); + assert!(lstat.contains(".Llstat_probe_fail:")); + assert!(lstat.contains("mov eax, 0xA1B6")); + assert!(lstat.contains("mov eax, 0xA124")); + assert!(lstat.contains("jz .Llstat_followed_fallback")); + assert!( + lstat.contains(".Llstat_followed_fallback:\n mov rdi, QWORD PTR [rsp + 144]\n mov rsi, QWORD PTR [rsp + 64]\n call __rt_sys_stat"), + "ordinary files and directories must reach the followed stat fallback" + ); +} + +/// Verifies uname retrieves the computer name as UTF-16 and converts it into the fixed +/// UTF-8 nodename field without retaining the ANSI hostname API. +#[test] +fn test_uname_uses_unicode_computer_name_layout() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_uname(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_uname"); + assert!(section.contains("sub rsp, 232")); + assert!(section.contains("call GetComputerNameW")); + assert!(!section.contains("call GetComputerNameA")); + assert!(section.contains("lea rdi, [rsp + 40]")); + assert!(section.contains("call __rt_win_utf16_to_utf8")); + assert!(section.contains("lea rcx, [rsp + 168]")); +} + +/// Verifies that `emit_win32_shims` unconditionally emits the +/// `__rt_unsupported_syscall` diagnostic helper (the target of the transform's +/// unmapped-syscall path), so it is always present for the transform to call. +#[test] +fn test_unsupported_syscall_helper_emitted() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_win32_shims(&mut emitter, RuntimeFeatures::all()); + let asm = emitter.output(); + assert!( + asm.contains(".globl __rt_unsupported_syscall\n"), + "emit_win32_shims must emit the __rt_unsupported_syscall diagnostic helper" + ); + assert!( + asm.contains("call ExitProcess"), + "the unsupported-syscall helper must terminate via ExitProcess" + ); +} + +/// Verifies that Winsock init/cleanup shims are emitted with the right Win32 calls. +#[test] +fn test_winsock_init_and_cleanup_emitted() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_win32_shims(&mut emitter, RuntimeFeatures::all()); + let asm = emitter.output(); + assert!(asm.contains(".globl __rt_winsock_init\n"), "winsock init shim missing"); + assert!(asm.contains("call WSAStartup"), "winsock init must call WSAStartup"); + assert!(asm.contains("0x0202"), "winsock init must load MAKEWORD(2,2)"); + assert!(asm.contains(".globl __rt_winsock_cleanup\n"), "winsock cleanup shim missing"); + assert!(asm.contains("call WSACleanup"), "winsock cleanup must call WSACleanup"); +} + +/// Verifies the disabled AF_UNIX registry owns no state and main only balances Winsock. +#[test] +fn test_main_wrapper_has_no_unix_loopback_teardown() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_unix_loopback_registry(&mut emitter); + emit_main_wrapper(&mut emitter); + let asm = emitter.output(); + let close_all = asm + .split(".globl __rt_win_unix_close_all\n") + .nth(1) + .expect("close-all helper missing"); + assert!(!close_all.contains("call closesocket")); + assert!(!close_all.contains("_win_unix_endpoint_records")); + let main = asm + .split(".globl main\n") + .nth(1) + .expect("Windows main wrapper missing"); + assert!(!main.contains("call __rt_win_unix_close_all")); + assert!(main.contains("call __rt_winsock_cleanup")); +} + +/// Verifies that `__rt_sys_exit` calls `__rt_winsock_cleanup` before `ExitProcess` +/// so Winsock resources are released on process termination. +#[test] +fn test_exit_calls_winsock_cleanup_before_exit_process() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_exit(&mut emitter); + let asm = emitter.output(); + let cleanup_pos = asm.find("call __rt_winsock_cleanup"); + let exit_pos = asm.find("call ExitProcess"); + assert!(cleanup_pos.is_some(), "exit shim must call winsock cleanup"); + assert!(exit_pos.is_some(), "exit shim must call ExitProcess"); + assert!(cleanup_pos < exit_pos, "winsock cleanup must run before ExitProcess"); +} + +/// Regression test for the Windows exit-crash class: `emit_shim_exit` starts with +/// `and rsp, -16` (forced alignment, since the shim can be reached at any +/// alignment), so the following `sub rsp, ` MUST have `N ≡ 0 mod 16` to keep +/// rsp ≡ 0 at the `call __rt_winsock_cleanup` and `call ExitProcess` sites. +/// Using `N ≡ 8 mod 16` (e.g. 40) would leave rsp ≡ 8 at both call sites — the +/// exact SSE #GP crash class that the original `and rsp, -16` fix was added for +/// (Wine's process-exit path reads aligned SSE registers). This test parses the +/// emitted asm, finds the `and rsp, -16` line, reads the next `sub rsp, `, +/// and asserts `N % 16 == 0`, locking the invariant so it can't regress. +#[test] +fn test_exit_shim_stack_alignment() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_exit(&mut emitter); + let asm = emitter.output(); + let lines: Vec<&str> = asm.lines().collect(); + // Find the `and rsp, -16` line, then the next `sub rsp, ` line. + let and_pos = lines + .iter() + .position(|l| l.trim().starts_with("and rsp, -16")) + .expect("exit shim must start with `and rsp, -16`"); + let sub_line = lines[and_pos + 1..] + .iter() + .find(|l| l.trim().starts_with("sub rsp, ")) + .expect("exit shim must have a `sub rsp, ` after `and rsp, -16`"); + let n_str = sub_line + .trim() + .strip_prefix("sub rsp, ") + .and_then(|rest| rest.split_whitespace().next()) + .expect("`sub rsp, ` must have a numeric operand"); + let n: u64 = n_str + .parse() + .unwrap_or_else(|_| panic!("`sub rsp, ` operand `{}` is not an integer", n_str)); + assert_eq!( + n % 16, + 0, + "exit shim: after `and rsp, -16` (forces rsp ≡ 0), `sub rsp, {}` must be ≡ 0 mod 16 \ + so rsp stays ≡ 0 at the Win32 call sites; got N ≡ {} mod 16 (misaligned → SSE #GP)", + n, + n % 16 + ); + // Both Win32 calls must appear after the aligned prologue. + assert!( + asm.contains("call __rt_winsock_cleanup"), + "exit shim must call __rt_winsock_cleanup" + ); + assert!( + asm.contains("call ExitProcess"), + "exit shim must call ExitProcess" + ); +} + +/// Verifies `access` implements PHP's Windows existence, readonly, and executable checks. +#[test] +fn test_access_shim_uses_get_file_attributes() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_access(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains(".globl __rt_sys_access\n")); + assert!(asm.contains("call __rt_win_utf8_to_utf16")); + assert!(asm.contains("call GetFileAttributesW")); + assert!(asm.contains("0xFFFFFFFF"), "access must check INVALID_FILE_ATTRIBUTES"); + assert!(asm.contains("test edx, 2"), "W_OK must inspect readonly attributes"); + assert!( + asm.contains( + "test eax, 1\n jnz .Laccess_eacces\n jmp .Laccess_acl_gate\n.Laccess_check_execute:" + ), + "a writable non-executable file must reach the DACL gate without falling through to X_OK" + ); + assert!(asm.contains("mov DWORD PTR [rip + __rt_errno], 13")); + assert!(asm.contains("test edx, 1"), "X_OK must use Windows executable detection"); + assert!(asm.contains("call GetBinaryTypeW")); + assert!(asm.contains(".Laccess_fail")); +} + +/// Verifies the ACL gate can only tighten a granted attribute answer, and that +/// every failure inside the check lands on the fail-open sentinel. +/// +/// The counts below are the complete branch inventory of `__rt_win_acl_grants`: +/// four `jz` (the size probe reported nothing, the descriptor read failed, no +/// impersonation token, `AccessCheck` itself failed), two `je` (the two UNC +/// prefix forms) and one `ja` (a descriptor larger than the frame's fixed +/// buffer) all target `.Lacl_unavailable`, which is the ONLY producer of the -1 +/// sentinel. Any new failure edge that does not land there would break the +/// fail-open invariant and change one of these numbers. +#[test] +fn test_access_acl_gate_fails_open_on_every_failure() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_win_acl_helpers(&mut emitter); + emit_shim_access(&mut emitter); + let asm = emitter.output(); + let gate = shim_section(&asm, "__rt_sys_access"); + assert!(gate.contains("call __rt_win_acl_grants")); + assert_eq!( + gate.matches("jz .Laccess_eacces").count(), + 1, + "the DACL verdict must be the only new denial in the shim" + ); + assert_eq!( + gate.matches(".Laccess_acl_gate").count(), + 3, + "both surviving attribute answers plus the gate's own label" + ); + let acl = shim_section(&asm, "__rt_win_acl_grants"); + assert_eq!(acl.matches("jz .Lacl_unavailable").count(), 4); + assert_eq!(acl.matches("je .Lacl_unavailable").count(), 2); + assert_eq!(acl.matches("ja .Lacl_unavailable").count(), 1); + assert_eq!( + acl.matches("mov rax, -1").count(), + 1, + "the fail-open sentinel has exactly one producer" + ); + assert_eq!(acl.matches("sub rsp, 2232").count(), 1); + assert_eq!(acl.matches("add rsp, 2232").count(), 2); + assert_eq!( + acl.matches("lea r8, [rsp + 176]").count(), + 1, + "the descriptor lives in the frame's fixed buffer" + ); + assert!( + acl.contains("mov r9d, 2048") && acl.contains("cmp eax, 2048"), + "the probed size must be bounded by the buffer actually passed" + ); + assert!( + !acl.contains("__rt_heap_alloc") && !acl.contains("__rt_heap_free"), + "the ACL path must not touch the heap: __rt_heap_alloc never returns NULL" + ); + assert!(!acl.contains("CloseHandle"), "the cached token is never closed here"); +} + +/// Verifies the impersonation token is opened at most once per process and that +/// a failed dance is remembered rather than retried. +#[test] +fn test_acl_token_cache_is_tri_state_and_reverts_only_its_own_impersonation() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_win_acl_helpers(&mut emitter); + let asm = emitter.output(); + let token = shim_section(&asm, "__rt_win_acl_token_get"); + assert!(token.contains("mov QWORD PTR [rip + _win_acl_token], -1")); + assert!(token.contains("mov QWORD PTR [rip + _win_acl_token], rax")); + assert_eq!(token.matches("call ImpersonateSelf").count(), 1); + assert_eq!(token.matches("call RevertToSelf").count(), 2); + assert_eq!(token.matches("call OpenThreadToken").count(), 2); + assert_eq!(token.matches("cmp QWORD PTR [rsp + 40], 0").count(), 2); + assert_eq!( + token.matches("mov edx, 8").count(), + 2, + "TOKEN_QUERY, not TOKEN_ALL_ACCESS, on both OpenThreadToken calls" + ); + assert_eq!(token.matches("sub rsp, 56").count(), 1); + assert_eq!(token.matches("add rsp, 56").count(), 2); + assert!(!token.contains("CloseHandle"), "the cached handle outlives every caller"); +} + +/// Verifies the advapi32 imports are declared alongside the rest of the table. +#[test] +fn test_win32_imports_declare_the_acl_family() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_win32_imports(&mut emitter); + let asm = emitter.output(); + for symbol in [ + "GetCurrentThread", + "OpenThreadToken", + "ImpersonateSelf", + "RevertToSelf", + "GetFileSecurityW", + "MapGenericMask", + "AccessCheck", + ] { + assert!(asm.contains(&format!(".extern {symbol}")), "{symbol} must be imported"); + } +} + +/// Verifies the ACL helpers reach the emitted Windows runtime and that the +/// process-wide token cache slot is declared for the Windows data section. +#[test] +fn test_win32_shims_emit_acl_helpers_and_token_slot() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_win32_shims(&mut emitter, RuntimeFeatures::all()); + let asm = emitter.output(); + assert!(asm.contains(".globl __rt_win_acl_token_get\n")); + assert!(asm.contains(".globl __rt_win_acl_grants\n")); + let helpers = asm + .find(".globl __rt_win_acl_grants\n") + .expect("ACL helper emitted"); + let access = asm.find(".globl __rt_sys_access\n").expect("access shim emitted"); + assert!( + helpers < access, + "the helpers must precede __rt_sys_access so shim_section slices cleanly" + ); + let data = crate::codegen_support::runtime::emit_runtime_data_fixed( + 1024 * 1024, + Target::new(Platform::Windows, Arch::X86_64), + ); + assert!( + data.contains(".comm _win_acl_token, 8, 3\n"), + "the tri-state token cache slot must exist on Windows" + ); +} + +/// Verifies `stat` mirrors php-src's Windows mode, hard-link, and special-handle +/// synthesis rather than collapsing every handle to a writable regular file. +#[test] +fn test_stat_shim_preserves_php_windows_modes_and_metadata() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_stat(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_stat"); + assert!(section.contains("call GetBinaryTypeW")); + assert!(section.contains("test edx, 0x10")); + assert!(section.contains("mov eax, 0x41FF")); + assert!(section.contains("or DWORD PTR [rsp + 156], 0x49")); + assert!(section.contains("and eax, 0xFF6D")); + assert!(section.contains("mov eax, DWORD PTR [rsp + 128]")); + assert!(section.contains("mov edx, DWORD PTR [rsp + 88]")); + assert!(section.contains(".Lstat_handle_mode_readonly")); + assert!(section.contains("mov rax, QWORD PTR [rsp + 100]")); + assert!(section.contains("mov rax, QWORD PTR [rsp + 108]")); + assert!(section.contains("mov rax, QWORD PTR [rsp + 92]")); + assert!(section.contains("call GetFileType")); + assert!(section.contains(".Lstat_pipe_mode")); + assert!(section.contains("mov eax, 0x11B6")); + assert!(section.contains("mov eax, 0x21B6")); + assert!(section.contains("sub rsp, 168")); + assert!(section.contains("add rsp, 168")); +} + +/// Verifies `access(X_OK | W_OK)` branches to the executable probe before +/// querying readonly attributes, matching php-src's X_OK short-circuit. +#[test] +fn test_access_shim_short_circuits_x_ok_before_w_ok() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_access(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_access"); + let execute_branch = section + .find("jnz .Laccess_check_execute") + .expect("X_OK branch must be emitted"); + let attributes_call = section + .find("call GetFileAttributesW") + .expect("non-X_OK access must still probe attributes"); + assert!( + execute_branch < attributes_call, + "X_OK must bypass readonly W_OK handling before GetFileAttributesW" + ); + assert!(section.contains("call GetBinaryTypeW")); + assert!(section.contains("mov edx, DWORD PTR [rsp + 40]")); +} + +/// Verifies `fstat` emits full handle metadata and pipe/character fallbacks. +#[test] +fn test_fstat_uses_by_handle_information_and_file_type_fallbacks() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_fstat(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("call __rt_fd_to_handle")); + assert!(asm.contains("call GetFileInformationByHandle")); + assert!(asm.contains("call GetFileType")); + assert!(asm.contains("mov eax, 0x11B6"), "pipes must be S_IFIFO"); + assert!(asm.contains("mov eax, 0x21B6"), "console/device handles must be S_IFCHR"); + assert!(asm.contains("mov eax, DWORD PTR [rsp + 72]"), "native link count must be loaded"); + assert!(asm.contains("test edx, 1"), "readonly attributes must clear write bits"); +} + +/// Verifies that `__rt_sys_ftruncate` converts CRT descriptors and preserves position. +#[test] +fn test_ftruncate_shim_uses_set_file_pointer_ex_and_set_end_of_file() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_ftruncate(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains(".globl __rt_sys_ftruncate\n")); + assert!(asm.contains("call __rt_fd_to_handle")); + assert_eq!(asm.matches("call SetFilePointerEx").count(), 3); + assert!(asm.contains("call SetEndOfFile")); + assert!(asm.contains("mov r9d, 1"), "the current position must be queried first"); + assert!(asm.contains("mov QWORD PTR [rsp + 40], rax"), "the converted HANDLE must be retained"); + assert!(asm.contains("mov rdx, QWORD PTR [rsp + 48]"), "the original position must be restored"); + assert!(asm.contains(".Lftruncate_fail")); + assert!(asm.contains(".Lftruncate_bad_fd")); + assert!(asm.contains(".Lftruncate_invalid_size")); + assert!(asm.contains("call GetLastError")); + assert!(asm.contains("call __rt_win32_errno_from_code")); + assert!(asm.contains("mov DWORD PTR [rip + __rt_errno], eax")); +} + +/// Verifies that the C-symbol stubs for `access`, `ftruncate`, and `umask` are +/// emitted so direct `call ` sites in the shared runtime resolve on Windows. +#[test] +fn test_c_symbol_stubs_for_access_ftruncate_umask() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_c_symbols(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains(".globl access\n"), "access C-symbol stub missing"); + assert!(asm.contains("call __rt_sys_access")); + assert!(asm.contains(".globl ftruncate\n"), "ftruncate C-symbol stub missing"); + assert!(asm.contains("call __rt_sys_ftruncate")); + assert!(asm.contains(".globl umask\n"), "umask C-symbol stub missing"); + let umask_section = shim_section(&asm, "umask"); + assert!(umask_section.contains("mov eax, DWORD PTR [rip + __rt_win_umask]")); + assert!(umask_section.contains("mov DWORD PTR [rip + __rt_win_umask], edi")); + assert!(!umask_section.contains("xor eax, eax"), "umask must preserve process-local state"); +} + +/// Verifies that WSAStartup, WSACleanup, SetFilePointerEx, and SetEndOfFile are +/// declared as Win32 imports so the MinGW linker resolves them against ws2_32/kernel32. +#[test] +fn test_new_win32_imports_declared() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_win32_shims(&mut emitter, RuntimeFeatures::all()); + let asm = emitter.output(); + assert!(asm.contains(".extern WSAStartup")); + assert!(asm.contains(".extern WSACleanup")); + assert!(asm.contains(".extern SetFilePointerEx")); + assert!(asm.contains(".extern SetEndOfFile")); +} + +/// Verifies that the `sleep` and `usleep` C-symbol stubs are emitted (so direct +/// `call sleep`/`call usleep` sites from the shared `lower_sleep`/`lower_usleep` +/// lowering resolve on Windows) and that both delegate to `Sleep`. +#[test] +fn test_sleep_usleep_c_symbol_stubs_emitted() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_c_symbols(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains(".globl sleep\n"), "sleep C-symbol stub missing"); + assert!(asm.contains(".globl usleep\n"), "usleep C-symbol stub missing"); + // Both stubs convert to milliseconds and call Win32 Sleep. + assert!(asm.contains("call Sleep"), "sleep/usleep must call Win32 Sleep"); + // sleep: seconds → ms via imul rcx, rdi, 1000. + let sleep_section = asm.split(".globl sleep\n").nth(1).unwrap_or(""); + assert!( + sleep_section.contains("imul rcx, rdi, 1000"), + "sleep must convert seconds→ms with imul rcx, rdi, 1000" + ); + // usleep: microseconds → ms via div by 1000. + let usleep_section = asm.split(".globl usleep\n").nth(1).unwrap_or(""); + assert!( + usleep_section.contains("div rcx"), + "usleep must convert usec→ms with a div" + ); +} + +/// Verifies that `__rt_sys_getrusage` is emitted, calls `GetProcessTimes`, uses +/// the current-process pseudo-handle (`mov rcx, -1`), and lays out the 5th +/// argument (lpUserTime) in the MSx64 stack-arg slot `[rsp + 32]`. +#[test] +fn test_getrusage_shim_uses_get_process_times() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_getrusage(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains(".globl __rt_sys_getrusage\n")); + assert!(asm.contains("call GetProcessTimes")); + assert!( + asm.contains("mov rcx, -1"), + "getrusage must use the current-process pseudo-handle (HANDLE)-1" + ); + // 5th arg (lpUserTime) goes in the MSx64 stack-arg slot at [rsp+32]. + assert!( + asm.contains("[rsp + 32], rax"), + "getrusage must pass lpUserTime via the [rsp+32] stack-arg slot" + ); + // FILETIME→timeval conversion uses the 10_000_000 divisor (100ns units per second). + assert!( + asm.contains("mov ecx, 10000000"), + "getrusage must divide FILETIME by 10_000_000 to get tv_sec" + ); + // RUSAGE_SELF guard branches and the two terminal paths. + assert!(asm.contains(".Lgetrusage_zero")); + assert!(asm.contains(".Lgetrusage_fail")); + assert!(asm.contains("rep stosq"), "getrusage must zero rusage fields with rep stosq"); + assert!( + asm.contains("mov rcx, 14"), + "getrusage success path must zero 14 qwords (ru_maxrss..ru_nivcsw, offsets 32..144)" + ); +} + +/// Verifies that `Sleep` and `GetProcessTimes` are declared as Win32 imports so +/// the MinGW linker resolves them against kernel32. +#[test] +fn test_sleep_getprocesstimes_imports_declared() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_win32_shims(&mut emitter, RuntimeFeatures::all()); + let asm = emitter.output(); + assert!(asm.contains(".extern Sleep")); + assert!(asm.contains(".extern GetProcessTimes")); +} + +/// Verifies that the `__rt_sys_getrusage` shim is registered in the full Win32 +/// shim set emitted by `emit_win32_shims` (so the syscall-98 transform target +/// resolves at link time). +#[test] +fn test_getrusage_shim_registered_in_full_set() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_win32_shims(&mut emitter, RuntimeFeatures::all()); + let asm = emitter.output(); + assert!(asm.contains(".globl __rt_sys_getrusage\n")); +} + +/// Verifies that the `popen`, `pclose`, `fileno`, `fgetc`, and `system` +/// C-symbol stubs are emitted with their `.globl` labels and that each body +/// calls the corresponding msvcrt import (`_popen`, `_pclose`, `_fileno`, +/// `fgetc`, `system`) — never the libc-name self-recursion form. +#[test] +fn test_popen_pclose_fileno_fgetc_system_stubs_emitted() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_c_symbols(&mut emitter); + let asm = emitter.output(); + for (name, import) in [ + ("popen", "call _popen"), + ("pclose", "call _pclose"), + ("fileno", "call _fileno"), + ("__rt_sys_fgetc", "call QWORD PTR [rip + __imp_fgetc]"), + ("__rt_sys_system", "call QWORD PTR [rip + __imp_system]"), + ] { + assert!( + asm.contains(&format!(".globl {}\n", name)), + "{} C-symbol stub missing", + name + ); + let section = asm + .split(&format!(".globl {}\n", name)) + .nth(1) + .unwrap_or(""); + assert!( + section.contains(import), + "{} stub must call {} (msvcrt import)", + name, + import + ); + } +} + +/// Verifies that the msvcrt imports `_popen`, `_pclose`, `_fileno`, `fgetc`, +/// `system` and the ws2_32 `select` import are declared as `.extern` so the +/// MinGW linker resolves them against msvcrt/ws2_32. +#[test] +fn test_popen_msvcrt_and_select_imports_declared() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_win32_shims(&mut emitter, RuntimeFeatures::all()); + let asm = emitter.output(); + assert!(asm.contains(".extern _popen"), "missing .extern _popen"); + assert!(asm.contains(".extern _pclose"), "missing .extern _pclose"); + assert!(asm.contains(".extern _fileno"), "missing .extern _fileno"); + assert!(asm.contains(".extern fgetc"), "missing .extern fgetc"); + assert!(asm.contains(".extern system"), "missing .extern system"); + assert!(asm.contains(".extern select"), "missing .extern select"); +} + +/// Verifies that the `__rt_sys_pselect6` shim calls ws2_32 `select` instead +/// of being the old `-1`/`ret` ENOSYS stub. +#[test] +fn test_pselect6_shim_calls_ws2_32_select() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_pselect6(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains(".globl __rt_sys_pselect6\n")); + assert!( + asm.contains("call select"), + "pselect6 shim must call ws2_32 select" + ); +} + +/// Verifies that the `__rt_sys_pselect6` shim's `sub rsp, ` frame size +/// satisfies `N % 16 == 8`, keeping rsp 16-byte aligned at the inner +/// `call select` (the shim is entered via `call` with rsp ≡ 8 mod 16). +#[test] +fn test_pselect6_shim_frame_stack_alignment() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_pselect6(&mut emitter); + let asm = emitter.output(); + let section = asm + .split(".globl __rt_sys_pselect6\n") + .nth(1) + .unwrap_or(""); + // First `sub rsp, ` after the label is the frame allocation. + let sub_line = section + .lines() + .find(|l| l.trim_start().starts_with("sub rsp,")) + .unwrap_or_else(|| panic!("no `sub rsp,` in pselect6 shim")); + let n: i64 = sub_line + .trim() + .trim_start_matches("sub rsp,") + .trim() + .parse() + .unwrap_or_else(|_| panic!("could not parse frame size from: {}", sub_line)); + assert_eq!( + n % 16, + 8, + "pselect6 frame size {} must satisfy N % 16 == 8", + n + ); +} + +/// Regression guard: with `RuntimeFeatures::none()`, `emit_win32_shims` must not +/// reference any zlib/bzip2/pcre2/iconv third-party symbol — those libraries are +/// linked only when the program actually uses them (`-lz -lbz2 -lpcre2-* -liconv`), +/// so an unconditional `call` to their symbols breaks the base MinGW link. A base +/// shim (`__rt_sys_write`) must still be present so gating did not eat the whole set. +#[test] +fn test_third_party_shims_gated_off_when_features_absent() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_win32_shims(&mut emitter, RuntimeFeatures::none()); + let asm = emitter.output(); + for call in [ + "call compress2", + "call BZ2_bzCompress", + "call pcre2_regcomp", + "call iconv_open", + ] { + assert!(!asm.contains(call), "features::none() must not emit `{}`", call); + } + for label in [ + "__rt_sys_compress2", + "__rt_sys_BZ2_bzCompress", + "__rt_sys_pcre2_regcomp", + "__rt_sys_iconv_open", + ] { + assert!( + !asm.contains(&format!(".globl {}\n", label)), + "features::none() must not emit shim label `{}`", + label + ); + } + assert!( + asm.contains(".globl __rt_sys_write\n"), + "base shims must remain emitted when third-party features are gated off" + ); +} + +/// Verifies that sysinfo initializes `MEMORYSTATUSEX.dwLength` before calling +/// `GlobalMemoryStatusEx` (required or the call always fails with +/// `ERROR_INVALID_PARAMETER`), and that the BOOL result is honored: success +/// returns 0, failure returns -1 (POSIX `sysinfo(2)` convention) instead of +/// the previous unconditional success. +#[test] +fn test_sysinfo_sets_dwlength_and_honors_bool_result() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_sysinfo(&mut emitter); + let asm = emitter.output(); + assert!( + asm.contains("mov DWORD PTR [rcx], 64"), + "dwLength must be initialised to sizeof(MEMORYSTATUSEX) before the call" + ); + assert!(asm.contains("call GlobalMemoryStatusEx")); + assert!(asm.contains("test eax, eax"), "must test the Win32 BOOL result"); + assert!(asm.contains(".Lsysinfo_ok"), "must branch to the success path"); + assert!(asm.contains("mov rax, -1"), "failure must translate to POSIX -1"); + assert!( + !asm.contains("xor rax, rax"), + "unconditional success return must be gone (now conditional on the BOOL)" + ); +} + +/// Verifies that kill returns -1 (not silent 0) for both the OpenProcess-failed +/// SIGKILL path and any unsupported (non-SIGKILL) signal — Windows has no +/// `posix_kill` equivalent, and a loud failure is safer than reporting success +/// for a signal that was never delivered. +#[test] +fn test_kill_returns_failure_for_unsupported_signal_and_open_process_failure() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_kill(&mut emitter); + let asm = emitter.output(); + // Locate the LABEL DEFINITIONS (`name:`), not the earlier jump-target + // references (`jne .Lsys_kill_noop`) — those share the bare name and + // would otherwise make `find` return the wrong (earlier) offset. + let fail_label_pos = asm + .find(".Lsys_kill_fail:\n") + .expect("fail label definition missing"); + let noop_label_pos = asm + .find(".Lsys_kill_noop:\n") + .expect("noop label definition missing"); + assert!(fail_label_pos < noop_label_pos, "fail block must precede noop block"); + let fail_section = &asm[fail_label_pos..noop_label_pos]; + let noop_section = &asm[noop_label_pos..]; + assert!( + fail_section.contains("mov rax, -1"), + "OpenProcess-failed path must return -1, not silent success" + ); + assert!( + noop_section.contains("mov rax, -1"), + "unsupported-signal path must return -1, not silent success" + ); + assert!( + !noop_section.contains("xor rax, rax"), + "no bare success return may remain on the unsupported-signal path" + ); +} + +/// Verifies the bare C `kill` symbol delegates to the same strict Win32 helper +/// instead of returning false success for unsupported signals or API failures. +#[test] +fn test_c_kill_symbol_delegates_to_strict_runtime_shim() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_c_symbols(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "kill"); + assert!(section.contains("call __rt_sys_kill")); + assert!(!section.contains("call OpenProcess")); + assert!(!section.contains("xor rax, rax")); +} + +/// Verifies that clock_getres writes `tv_nsec = 1` (matching the "1ns +/// best-effort resolution" docblock) into the caller's `struct timespec`, and +/// NULL-guards the write since POSIX permits `res == NULL`. +#[test] +fn test_clock_getres_writes_resolution_and_null_guards() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_clock_getres(&mut emitter); + let asm = emitter.output(); + assert!( + asm.contains("test rsi, rsi"), + "must NULL-guard the res pointer before writing" + ); + assert!( + asm.contains(".Lclock_getres_done"), + "must skip the write when res is NULL" + ); + assert!( + asm.contains("mov QWORD PTR [rsi + 8], 1"), + "tv_nsec must be written as 1 (1ns best-effort resolution)" + ); +} + +/// Verifies `GetLastError` is declared as a Win32 import — required by +/// `__rt_sys_read`'s failure path (finding F3, the Windows errno layer). +#[test] +fn test_get_last_error_declared_for_errno_translation() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_win32_shims(&mut emitter, RuntimeFeatures::all()); + let asm = emitter.output(); + assert!( + asm.contains(".extern GetLastError"), + "GetLastError must be declared for __rt_sys_read's failure path" + ); +} + +/// Verifies the full Win32 shim emission wires in the errno translation +/// helper (called from `emit_shim_c_symbols`, not registered separately in +/// `emit_win32_shims`). +#[test] +fn test_full_win32_shim_emission_includes_errno_translate_helper() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_win32_shims(&mut emitter, RuntimeFeatures::all()); + let asm = emitter.output(); + assert!(asm.contains(".globl __rt_win32_errno_from_code\n")); +} + +/// Windows errno layer (finding F3): `__rt_win32_errno_from_code` translates a +/// Win32/WSA `GetLastError`/`WSAGetLastError` code into the POSIX errno value +/// the `fgets`/`fread` nonblocking-read check compares against 11 (EAGAIN) — +/// see `io/fgets.rs:271-274` and `io/fread.rs:227-230`. +mod errno_translation { + use super::*; + + /// Verifies the three behaviorally-critical would-block codes all map to + /// EAGAIN(11): this is what lets fgets/fread distinguish a nonblocking + /// would-block miss from real EOF/error on Windows. + #[test] + fn test_win32_errno_translate_would_block_codes_map_to_eagain() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_win32_errno_translate(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains(".globl __rt_win32_errno_from_code\n")); + for code in ["10035", "232", "997"] { + assert!( + asm.contains(&format!("cmp ecx, {}", code)), + "translate helper must compare against Win32/WSA code {}", + code + ); + } + // All three would-block codes branch to the same EAGAIN(11) terminal. + let eagain_pos = asm + .find(".Lw32err_eagain:\n") + .expect("EAGAIN terminal label missing"); + assert!( + asm[eagain_pos..].contains("mov eax, 11"), + "EAGAIN terminal must return 11" + ); + } + + /// Verifies the common Win32/WSA -> POSIX errno mappings are present as + /// distinct compare targets, that ERROR_HANDLE_EOF(38) maps to 0 (not an + /// error), and that an unrecognized code falls through to EIO(5) rather + /// than a garbage value. + #[test] + fn test_win32_errno_translate_common_codes_and_unknown_default() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_win32_errno_translate(&mut emitter); + let asm = emitter.output(); + for code in ["2", "3", "5", "6", "109", "38", "10004", "10054", "10060"] { + assert!( + asm.contains(&format!("cmp ecx, {}", code)), + "translate helper must compare against Win32/WSA code {}", + code + ); + } + let eof_pos = asm + .find(".Lw32err_eof:\n") + .expect("EOF terminal label missing"); + assert!( + asm[eof_pos..].contains("xor eax, eax"), + "ERROR_HANDLE_EOF must translate to 0 (not an error)" + ); + // The straight-line fallthrough before the first labeled terminal is + // the "no compare matched" default path -> EIO(5). + let first_label_pos = asm + .find(".Lw32err_eagain:\n") + .expect("first terminal label missing"); + let default_zone = &asm[..first_label_pos]; + assert!( + default_zone.contains("mov eax, 5"), + "unrecognized codes must default to EIO(5)" + ); + assert!( + default_zone.trim_end().ends_with("ret"), + "the default EIO path must return immediately" + ); + } +} + +/// Verifies `__rt_sys_read` (the ReadFile shim) stores a translated errno on +/// failure and returns -1, but leaves errno untouched and still returns the +/// byte count on success — the fgets/fread EAGAIN check needs both halves of +/// this contract (`io/fgets.rs:271-274`, `io/fread.rs:227-230`). +#[test] +fn test_sys_read_stores_errno_on_readfile_failure_only() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_read(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_read"); + assert!(section.contains("call ReadFile")); + assert!(section.contains("test eax, eax"), "must test the ReadFile BOOL result"); + assert!( + section.contains("jz .Lsys_read_fail"), + "must branch to the failure path when the BOOL result is 0" + ); + let fail_pos = section.find(".Lsys_read_fail:\n").expect("failure label missing"); + let (success_zone, fail_zone) = section.split_at(fail_pos); + let readfile_pos = success_zone + .find(".Lsys_read_file:\n") + .expect("ReadFile path label missing"); + let socket_pos = success_zone + .find(".Lsys_read_socket:\n") + .expect("socket path label missing"); + let readfile_success_zone = &success_zone[readfile_pos..socket_pos]; + assert!( + !readfile_success_zone.contains("__rt_errno") && !readfile_success_zone.contains("GetLastError"), + "the successful ReadFile path must not touch errno (POSIX does not clear errno on success)" + ); + assert!( + readfile_success_zone.contains("mov eax, DWORD PTR [rsp + 48]"), + "success path must still return the ReadFile byte count" + ); + assert!( + fail_zone.contains("call GetLastError"), + "failure path must fetch GetLastError()" + ); + assert!( + fail_zone.contains("call __rt_win32_errno_from_code"), + "failure path must translate the Win32 code to a POSIX errno" + ); + assert!( + fail_zone.contains("mov DWORD PTR [rip + __rt_errno], eax"), + "failure path must publish the translated errno" + ); + assert!( + fail_zone.contains("mov rax, -1"), + "failure path must return -1 like POSIX read()" + ); +} + +/// Verifies a cached `O_NONBLOCK` CRT pipe polls availability like php-src +/// before the synchronous ReadFile call, while a POSIX zero-length read +/// returns zero without observing readiness or publishing EAGAIN. +#[test] +fn test_sys_read_uses_peek_named_pipe_for_nonblocking_crt_pipes() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_read(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_read"); + assert!(section.contains("call __rt_win_fd_status_find")); + assert!(section.contains("test QWORD PTR [rax + 16], 0x800")); + assert!(section.contains("call PeekNamedPipe")); + assert!(section.contains(".Lsys_read_nonblocking_pipe_probe:")); + assert!(section.contains("inc QWORD PTR [rsp + 96]")); + assert!(section.contains("cmp QWORD PTR [rsp + 96], 32000")); + assert!(section.contains("call Sleep")); + assert!(section.contains("jmp .Lsys_read_nonblocking_pipe_probe")); + let ready_pos = section + .find(".Lsys_read_nonblocking_pipe_ready:\n") + .expect("nonblocking pipe ready label missing"); + let readfile_pos = section + .find(".Lsys_read_file:\n") + .expect("ReadFile path label missing"); + let ready_zone = §ion[ready_pos..readfile_pos]; + assert!(ready_zone.contains("mov eax, DWORD PTR [rsp + 56]")); + assert!(ready_zone.contains("cmova rdx, rax")); + assert!(ready_zone.contains("mov QWORD PTR [rsp + 72], rdx")); + let zero_pos = section + .find(".Lsys_read_nonempty:\n") + .expect("zero-length read fast path missing"); + let zero_path = §ion[..zero_pos]; + assert!(zero_path.contains("test rdx, rdx")); + assert!(zero_path.contains("xor eax, eax")); + assert!( + !zero_path.contains("PeekNamedPipe") && !zero_path.contains("ReadFile"), + "zero-length reads must complete before any Win32 pipe operation" + ); +} + +/// Verifies `__rt_sys_recvfrom` stores a translated errno on `SOCKET_ERROR` +/// and still returns -1 (sign-extended), while the success path is untouched +/// and still `cdqe`s the byte count (Class-3 sign-extension requirement). +#[test] +fn test_recvfrom_stores_errno_on_socket_error() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_socket_shims(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_recvfrom"); + assert!( + section.contains("cmp eax, -1"), + "must test for SOCKET_ERROR after recvfrom" + ); + assert!(section.contains("je .Lrecvfrom_fail")); + let fail_pos = section.find(".Lrecvfrom_fail:\n").expect("failure label missing"); + let (success_zone, fail_zone) = section.split_at(fail_pos); + assert!( + success_zone.contains("cdqe"), + "success path must still sign-extend the byte count" + ); + assert!(fail_zone.contains("call __rt_wsa_capture_errno")); +} + +/// Regression test for WF10b BUG A: `__rt_sys_snprintf_double` (used by +/// `__rt_sprintf`'s `%f`/`%e`/`%g`/`%E`/`%G` path, whose +/// `snprintf(buf, size, fmt, double)` call has NO leading integer +/// precision argument, unlike `__rt_ftoa`'s `snprintf(buf, size, "%.*e", p, x)` +/// shape handled by `__rt_sys_snprintf`) must stage buf/size/fmt into +/// rcx/rdx/r8 and duplicate the double into BOTH r9 (the MSx64 positional +/// argument-4 integer register, required for a variadic callee's `va_arg` +/// home-slot read) and xmm3 (the positional argument-4 float register) before +/// calling MinGW `snprintf` — NOT the `[rsp+32]` 5th-argument stack slot +/// `__rt_sys_snprintf` uses, since here the double is positional argument 4, +/// not 5. +#[test] +fn test_snprintf_double_shim_duplicates_double_into_r9_and_xmm3() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_snprintf_double(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_snprintf_double"); + assert!(section.contains("mov r8, rdx"), "fmt -> arg3 (r8)"); + assert!(section.contains("mov rdx, rsi"), "size -> arg2 (rdx)"); + assert!(section.contains("mov rcx, rdi"), "buf -> arg1 (rcx)"); + assert!( + section.contains("movq r9, xmm0"), + "the double must be duplicated into r9 (arg4 integer register)" + ); + assert!( + section.contains("movaps xmm3, xmm0"), + "the double must be duplicated into xmm3 (arg4 float register)" + ); + assert!(section.contains("call __mingw_snprintf")); + assert!( + !section.contains("[rsp + 32]"), + "unlike __rt_sys_snprintf, the double here is positional arg4 (register-only), not a 5th stack arg" + ); +} + +/// Verifies the precision-shaped snprintf bridge puts its integer variadic +/// argument in MSx64 position four and its double in the position-five stack +/// slot, rather than incorrectly duplicating the stack argument into XMM3/R9. +#[test] +fn test_snprintf_precision_shim_places_double_in_fifth_stack_slot() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_snprintf(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_snprintf"); + assert!(section.contains("mov r9, r10"), "precision -> arg4 (r9)"); + assert!( + section.contains("movsd QWORD PTR [rsp + 32], xmm0"), + "the positional arg5 double must occupy the stack slot above shadow space" + ); + assert!(!section.contains("movaps xmm3, xmm0")); + assert!(section.contains("call __mingw_snprintf")); +} + +/// Verifies negative msvcrt `strtol` results are widened from Windows' 32-bit +/// `long` representation before 64-bit runtime callers inspect the sign. +#[test] +fn test_strtol_shim_sign_extends_windows_long() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_strtol(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_strtol"); + let call = section.find("call strtol").expect("missing strtol import call"); + let widen = section.find("cdqe").expect("missing signed-long widening"); + assert!(call < widen); +} + +/// Verifies native filesystem shims import strict UTF conversion and wide Win32 APIs. +#[test] +fn test_unicode_filesystem_imports_are_declared() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_win32_shims(&mut emitter, RuntimeFeatures::all()); + let asm = emitter.output(); + for symbol in [ + "MultiByteToWideChar", + "WideCharToMultiByte", + "FindFirstFileExW", + "FindNextFileW", + "CreateFileW", + "GetFinalPathNameByHandleW", + "GetFileAttributesExW", + "GetCurrentDirectoryW", + "GetDiskFreeSpaceExW", + "GetTempPathW", + "GetCommandLineW", + "CommandLineToArgvW", + "LocalFree", + "SetFileAttributesW", + ] { + assert!(asm.contains(&format!(".extern {symbol}\n")), "missing import {symbol}"); + } +} + +/// Verifies the reverse conversion stages all eight `WideCharToMultiByte` arguments. +#[test] +fn test_utf16_to_utf8_helper_uses_widechar_msx64_layout() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_win32_encoding_helpers(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_win_utf16_to_utf8"); + assert!(section.contains("mov ecx, 65001")); + assert!(section.contains("mov edx, 128")); + assert!(section.contains("mov r8, rdi")); + assert!(section.contains("mov r9d, -1")); + assert!(section.contains("mov QWORD PTR [rsp + 32], rsi")); + assert!(section.contains("mov QWORD PTR [rsp + 40], rdx")); + assert!(section.contains("call WideCharToMultiByte")); +} + +/// Verifies directory enumeration retains a wide pattern and converts each name to UTF-8. +#[test] +fn test_directory_shims_use_unicode_enumeration() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_opendir(&mut emitter); + emit_shim_readdir(&mut emitter); + emit_shim_rewinddir(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("call __rt_win_utf8_to_utf16")); + assert_eq!(asm.matches("call FindFirstFileExW").count(), 2); + assert!(asm.contains("call FindNextFileW")); + assert!(asm.contains("call __rt_win_utf16_to_utf8")); + assert!(!asm.contains("call FindFirstFileA")); + assert!(!asm.contains("call FindNextFileA")); +} + +/// Verifies directory EOF remains distinct from native enumeration/conversion +/// failures and `closedir` reports a failed `FindClose` after releasing memory. +#[test] +fn test_directory_shims_publish_native_errors() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_readdir(&mut emitter); + emit_shim_closedir(&mut emitter); + let asm = emitter.output(); + let readdir = shim_section(&asm, "__rt_sys_readdir"); + assert!(readdir.contains("call GetLastError")); + assert!(readdir.contains("cmp eax, 18")); + assert!(readdir.contains(".Lreaddir_native_error:")); + assert!(readdir.contains("call __rt_win32_errno_from_code")); + let closedir = shim_section(&asm, "__rt_sys_closedir"); + assert!(closedir.contains("call FindClose")); + assert!(closedir.contains("call GetLastError")); + assert!(closedir.contains("call __rt_win32_errno_from_code")); + assert!(closedir.contains("mov rax, -1")); +} + +/// Verifies glob uses wide matching while fnmatch keeps PHP's POSIX flag semantics. +#[test] +fn test_glob_and_fnmatch_use_wide_windows_apis() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_c_symbols(&mut emitter); + let asm = emitter.output(); + let glob = shim_section(&asm, "glob"); + assert!(glob.contains("call __rt_win_utf8_to_utf16")); + assert!(glob.contains("call FindFirstFileExW")); + assert!(glob.contains("call FindNextFileW")); + assert!(glob.contains("call __rt_win_utf16_to_utf8")); + assert!(!glob.contains("call FindFirstFileA")); + let fnmatch = shim_section(&asm, "fnmatch"); + assert!(!fnmatch.contains("PathMatchSpec")); + assert!(fnmatch.contains("test QWORD PTR [rsp], 1")); + assert!(fnmatch.contains("test QWORD PTR [rsp], 2")); + assert!(fnmatch.contains("test QWORD PTR [rsp + 8], 4")); + assert!(fnmatch.contains("test QWORD PTR [rsp], 16")); +} + +/// Verifies Windows `glob` grows its result vector without a fixed match ceiling +/// and completely releases partial ownership when growth cannot be represented. +#[test] +fn test_glob_result_vector_grows_dynamically_and_cleans_up_on_enomem() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_c_symbols(&mut emitter); + let asm = emitter.output(); + let glob = shim_section(&asm, "glob"); + assert!(glob.contains("mov QWORD PTR [rsp + 704], 16")); + assert!(glob.contains("lea rax, [rcx + rcx]")); + assert!(glob.contains(".Lglob_grow_copy:")); + assert!(glob.contains("mov QWORD PTR [rsp + 696], rax")); + assert!(!glob.contains("cmp rax, 1024")); + assert!(!glob.contains("silently drop matches")); + assert!(glob.contains("cmp rcx, 0x10000000")); + assert!(glob.contains("shr r10, 32")); + assert!(glob.contains(".Lglob_nospace_path_loop:")); + assert!(glob.contains("call FindClose")); + assert!(glob.contains("mov DWORD PTR [rip + __rt_errno], 12")); + assert!(glob.contains("mov QWORD PTR [rcx + 8], 0")); +} + +/// Verifies `mkstemp` atomically creates and attributes the generated Unicode path. +#[test] +fn test_mkstemp_uses_wide_atomic_creation() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_mkstemp(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("call __rt_win_utf8_to_utf16")); + assert!(asm.contains("call CreateFileW")); + assert!(asm.contains("call SetFileAttributesW")); + assert!(asm.contains("call _open_osfhandle")); + assert!(asm.contains("cmp eax, -1")); + assert!(asm.contains("mov edx, 0x8002")); + assert!(asm.contains("mov QWORD PTR [rsp + 32], 1")); + assert!(!asm.contains("call CreateFileA")); +} + +/// Verifies path-mutating shims preserve UTF-8 at the runtime edge and call only `W` APIs. +#[test] +fn test_unicode_path_mutation_shims_use_strict_wide_boundary() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_unlink(&mut emitter); + emit_shim_chdir(&mut emitter); + emit_shim_mkdir(&mut emitter); + emit_shim_rmdir(&mut emitter); + emit_shim_chmod(&mut emitter); + emit_shim_access(&mut emitter); + emit_shim_rename(&mut emitter); + let asm = emitter.output(); + for (label, api) in [ + ("__rt_sys_unlink", "DeleteFileW"), + ("__rt_sys_chdir", "SetCurrentDirectoryW"), + ("__rt_sys_mkdir", "CreateDirectoryW"), + ("__rt_sys_rmdir", "RemoveDirectoryW"), + ("__rt_sys_chmod", "SetFileAttributesW"), + ("__rt_sys_access", "GetFileAttributesW"), + ("__rt_sys_rename", "MoveFileExW"), + ] { + let section = shim_section(&asm, label); + assert!(section.contains("call __rt_win_utf8_to_utf16"), "{label} must preserve UTF-8/UTF-16 boundary"); + assert!(section.contains(&format!("call {api}")), "{label} must call {api}"); + assert!(section.contains("__rt_errno"), "{label} must publish conversion/native failure"); + } + for label in [ + "__rt_sys_unlink", + "__rt_sys_chdir", + "__rt_sys_mkdir", + "__rt_sys_rmdir", + ] { + let section = shim_section(&asm, label); + assert!( + section.contains("xor eax, eax") && section.contains("mov eax, -1"), + "{label} must translate Win32 BOOL into POSIX 0/-1 status" + ); + assert!( + section.matches("cdqe").count() >= 2, + "{label} must sign-extend native and UTF-8 conversion failure sentinels" + ); + } + for ansi in [ + "DeleteFileA", + "SetCurrentDirectoryA", + "CreateDirectoryA", + "RemoveDirectoryA", + "SetFileAttributesA", + "GetFileAttributesA", + "MoveFileExA", + ] { + assert!(!asm.contains(&format!("call {ansi}")), "ANSI path call remains: {ansi}"); + } +} + +/// Verifies the strict conversion boundary does not normalize UNC or extended path prefixes. +#[test] +fn test_utf8_path_helper_delegates_unc_and_long_path_bytes_without_rewrite() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_win32_encoding_helpers(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_win_utf8_to_utf16"); + assert!(section.contains("call MultiByteToWideChar")); + assert!(!section.contains("GetFullPathName")); + assert!(!section.contains("PathCch")); + assert!(!section.contains("PathCanonicalize")); +} + +/// Verifies `getcwd` dynamically queries a wide long path, converts it into the caller's +/// UTF-8 buffer, returns that buffer, and reports native/range/allocation failures. +#[test] +fn test_getcwd_uses_dynamic_wide_buffer_and_utf8_result() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_getcwd(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_getcwd"); + assert_eq!(section.matches("call GetCurrentDirectoryW").count(), 2); + assert!(!section.contains("call GetCurrentDirectoryA")); + assert!(section.contains("xor ecx, ecx")); + assert!(section.contains("call __rt_heap_alloc")); + assert!(section.contains("call __rt_win_utf16_to_utf8")); + assert!(section.contains("mov rax, QWORD PTR [rsp + 32]")); + assert!(section.contains("call __rt_heap_free")); + assert!(section.contains("mov DWORD PTR [rip + __rt_errno], 34")); + assert!(section.contains("mov DWORD PTR [rip + __rt_errno], 12")); +} + +/// Verifies `statfs` converts Unicode/extended paths and preserves the Windows output +/// qwords separately from its owned path and error locals. +#[test] +fn test_statfs_uses_wide_disk_space_layout_and_errno() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_statfs(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_statfs"); + assert!(section.contains("sub rsp, 88")); + assert!(section.contains("call __rt_win_utf8_to_utf16")); + assert!(section.contains("lea rdx, [rsp + 32]")); + assert!(section.contains("lea r8, [rsp + 40]")); + assert!(section.contains("mov QWORD PTR [rsp + 48], rax")); + assert!(section.contains("mov QWORD PTR [rsp + 64], rsi")); + assert!(section.contains("mov rsi, QWORD PTR [rsp + 64]")); + assert!(section.contains("call GetDiskFreeSpaceExW")); + assert!(!section.contains("call GetDiskFreeSpaceExA")); + assert!(section.contains("mov DWORD PTR [rsi + 8], 1")); + assert!(section.contains("mov QWORD PTR [rsi + 16], rax")); + assert!(section.contains("mov QWORD PTR [rsi + 32], rax")); + assert!(section.contains("call __rt_heap_free")); + assert!(section.contains("__rt_errno")); +} + +/// Verifies the temp-directory helper queries arbitrary wide lengths, performs a strict +/// two-pass UTF-8 conversion, trims the trailing separator, and balances both allocations. +#[test] +fn test_temp_dir_uses_dynamic_wide_utf8_pipeline() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_sys_get_temp_dir(&mut emitter); + let asm = emitter.output(); + let section = shim_section(&asm, "__rt_sys_get_temp_dir"); + assert_eq!(section.matches("call GetTempPathW").count(), 2); + assert!(!section.contains("call GetTempPathA")); + assert!(!section.contains("mov ecx, 260")); + assert_eq!(section.matches("call __rt_win_utf16_to_utf8").count(), 2); + assert_eq!(section.matches("call __rt_heap_alloc").count(), 2); + assert!(section.matches("call __rt_heap_free").count() >= 4); + assert!(section.contains("cmp BYTE PTR [r10 + rax - 1], 92")); + assert!(section.contains("mov BYTE PTR [r10 + rax], 0")); + assert!(section.contains("__rt_win32_last_error")); + assert!(section.contains("__rt_errno")); +} + +/// Verifies Win32 and Winsock native error states remain distinct from POSIX errno. +#[test] +fn test_native_error_slots_are_separate() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_c_symbols(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("__rt_errno:\n")); + assert!(asm.contains("__rt_win32_last_error:\n")); + assert!(asm.contains("__rt_wsa_last_error:\n")); +} + +/// Verifies PHP-visible native error messages originate as Unicode system messages. +#[test] +fn test_win32_error_message_uses_format_message_w_and_utf8_output() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_win32_error_message(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("mov ecx, 0x1200")); + assert!(asm.contains("call FormatMessageW")); + assert!(asm.contains("call WideCharToMultiByte")); + assert!(asm.contains("call __rt_win_utf16_to_utf8")); + assert!(asm.contains("call __rt_heap_alloc")); + assert!(asm.contains("call __rt_heap_free")); +} + +/// Verifies failure-only capture keeps Win32 and Winsock native errors separate. +#[test] +fn test_native_errno_capture_helpers_translate_without_success_side_effects() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_native_errno_capture_helpers(&mut emitter); + let asm = emitter.output(); + let win32 = shim_section(&asm, "__rt_win32_capture_errno"); + assert!(win32.contains("call GetLastError")); + assert!(win32.contains("__rt_win32_last_error")); + assert!(win32.contains("call __rt_win32_errno_from_code")); + let wsa = shim_section(&asm, "__rt_wsa_capture_errno"); + assert!(wsa.contains("call WSAGetLastError")); + assert!(wsa.contains("__rt_wsa_last_error")); + assert!(wsa.contains("call __rt_win32_errno_from_code")); +} + +/// Verifies all reachable core Winsock shims route failures through errno capture. +#[test] +fn test_reachable_socket_shims_capture_errno() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_socket_shims(&mut emitter); + emit_shim_accept4(&mut emitter); + emit_shim_setsockopt(&mut emitter); + emit_shim_getsockopt(&mut emitter); + let asm = emitter.output(); + for label in [ + "__rt_sys_socket", + "__rt_sys_accept", + "__rt_sys_bind", + "__rt_sys_listen", + "__rt_sys_connect", + "__rt_sys_shutdown", + "__rt_sys_getsockname", + "__rt_sys_getpeername", + "__rt_sys_closesocket", + "__rt_sys_sendto", + "__rt_sys_recvfrom", + "__rt_sys_setsockopt", + "__rt_sys_getsockopt", + ] { + assert!( + shim_section(&asm, label).contains("call __rt_wsa_capture_errno"), + "{label} must capture Winsock errno on failure" + ); + } + assert!( + shim_section(&asm, "__rt_sys_accept4").contains("call __rt_sys_accept"), + "accept4 must delegate to the errno-capturing accept shim" + ); +} + +/// Verifies high-impact filesystem and connection failures map to POSIX errno values. +#[test] +fn test_extended_errno_mapping_includes_connection_and_path_failures() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_win32_errno_translate(&mut emitter); + let asm = emitter.output(); + for (code, label, errno) in [ + ("10061", ".Lw32err_econnrefused", "111"), + ("10048", ".Lw32err_eaddrinuse", "98"), + ("112", ".Lw32err_enospc", "28"), + ("183", ".Lw32err_eexist", "17"), + ("206", ".Lw32err_enametoolong", "36"), + ("122", ".Lw32err_erange", "34"), + ("1113", ".Lw32err_eilseq", "84"), + ] { + assert!(asm.contains(&format!("cmp ecx, {code}")), "missing native code {code}"); + let terminal = asm.find(&format!("{label}:\n")).expect("mapping terminal missing"); + assert!(asm[terminal..].contains(&format!("mov eax, {errno}"))); + } +} +/// Verifies the unsupported futex syscall fails loudly instead of claiming +/// synchronization succeeded without sleeping or waking any waiter. +#[test] +fn test_futex_never_reports_false_success() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_futex(&mut emitter); + let section = emitter.output(); + + assert!(section.contains("mov DWORD PTR [rip + __rt_errno], 38")); + assert!(section.contains("mov rax, -1")); + assert!(!section.contains("xor rax, rax")); +} + +/// Verifies PROT_NONE becomes a real inaccessible Windows guard page. +#[test] +fn test_mprotect_translates_prot_none_to_page_noaccess() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_mprotect(&mut emitter); + let section = emitter.output(); + + assert!(section.contains("test edx, edx")); + assert!(section.contains("mov r8, 0x01")); + assert!(section.contains("call VirtualProtect")); +} + +/// Verifies mmap reserves and commits a region that VirtualProtect can guard. +#[test] +fn test_mmap_reserves_and_commits_protectable_region() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_shim_mmap(&mut emitter); + let section = emitter.output(); + + assert!(section.contains("mov r8, 0x3000")); + assert!(section.contains("call VirtualAlloc")); +} + +/// Verifies the low-stack Fiber abort path performs only a minimal aligned +/// `ExitProcess` call and cannot return into Wine's exception dispatcher. +#[test] +fn test_fiber_stack_overflow_abort_exits_without_cleanup() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_fiber_stack_overflow_abort(&mut emitter); + let section = emitter.output(); + + assert!(section.contains("and rsp, -16")); + assert!(section.contains("sub rsp, 32")); + assert!(section.contains("mov ecx, 134")); + assert!(section.contains("call ExitProcess")); + assert!(section.contains("ud2")); + assert!(!section.contains("__rt_sys_free_argv")); + assert!(!section.contains("__rt_winsock_cleanup")); +} diff --git a/src/codegen_support/runtime_features.rs b/src/codegen_support/runtime_features.rs index 744645fcf7..9e82fe3934 100644 --- a/src/codegen_support/runtime_features.rs +++ b/src/codegen_support/runtime_features.rs @@ -50,6 +50,28 @@ pub struct RuntimeFeatures { /// tail-call `elephc_web_write` (a symbol only linked into `--web` binaries). /// Non-web runtimes must leave this false so they never reference that symbol. pub web: bool, + /// True when the program links zlib (`-lz`). Gates the windows-x86_64 + /// `__rt_sys_{compress2,deflate,inflate,…}` ABI shims so a program that + /// never compresses does not reference zlib symbols the base MinGW link + /// cannot resolve. No effect on non-Windows targets (shims are Windows-only). + pub zlib: bool, + /// True when the program links libbz2 (`-lbz2`). Gates the windows-x86_64 + /// `__rt_sys_BZ2_*` ABI shims so a program that never uses bzip2 stream + /// filters does not reference libbz2 symbols the base MinGW link cannot + /// resolve. No effect on non-Windows targets (shims are Windows-only). + pub bzip2: bool, + /// True when the program links libiconv (`-liconv`). Gates the windows-x86_64 + /// `__rt_sys_iconv_*` ABI shims so a program that never uses iconv stream + /// filters does not reference libiconv symbols the base MinGW link cannot + /// resolve. No effect on non-Windows targets (shims are Windows-only). + pub iconv: bool, + /// True when generated user assembly publishes or calls elephc-tls bridge exports. + /// Gates Windows SysV-to-MS x64 adapters so non-TLS binaries keep no bridge references. + pub tls: bool, + /// True when Windows date/time lowering publishes the elephc-tz offset export. + /// Set only for Windows EIR modules, so the target-neutral link-library + /// derivation can request the bridge without imposing it on Unix targets. + pub timezone_bridge: bool, } impl RuntimeFeatures { @@ -63,6 +85,11 @@ impl RuntimeFeatures { eval_bridge: false, eval_scope: false, web: false, + zlib: false, + bzip2: false, + iconv: false, + tls: false, + timezone_bridge: false, } } @@ -77,6 +104,11 @@ impl RuntimeFeatures { eval_bridge: true, eval_scope: true, web: true, + zlib: true, + bzip2: true, + iconv: true, + tls: true, + timezone_bridge: true, } } } @@ -104,8 +136,9 @@ pub fn required_libraries_for_runtime_features(features: RuntimeFeatures) -> Vec } if features.phar_archive { libs.push("elephc_phar".to_string()); - libs.push("z".to_string()); - libs.push("bz2".to_string()); + } + if features.timezone_bridge { + libs.push("elephc_tz".to_string()); } if features.descriptor_invoker { // The dynamic builtin dispatcher emits md5/sha1/hash wrappers that @@ -995,6 +1028,31 @@ mod tests { assert!(required_libraries_for_runtime_features(RuntimeFeatures::none()).is_empty()); } + /// Verifies PHAR runtime support links only its self-contained Rust bridge. + #[test] + fn test_phar_runtime_features_require_only_elephc_phar() { + assert_eq!( + required_libraries_for_runtime_features(RuntimeFeatures { + phar_archive: true, + ..RuntimeFeatures::none() + }), + vec!["elephc_phar".to_string()] + ); + } + + /// Verifies Windows date/time lowering can request the timezone bridge + /// through the target-neutral runtime-feature link derivation. + #[test] + fn test_timezone_runtime_feature_requires_elephc_tz() { + assert_eq!( + required_libraries_for_runtime_features(RuntimeFeatures { + timezone_bridge: true, + ..RuntimeFeatures::none() + }), + vec!["elephc_tz".to_string()] + ); + } + /// Verifies eval runtime features request PCRE2 plus the magician bridge staticlib for final linking. #[test] fn test_eval_runtime_features_require_elephc_magician_library() { @@ -1166,13 +1224,8 @@ mod tests { #[test] fn test_descriptor_invoker_runtime_features_require_elephc_crypto_library() { assert!(required_libraries_for_runtime_features(RuntimeFeatures { - regex: false, - mb_strlen: false, - phar_archive: false, descriptor_invoker: true, - eval_bridge: false, - eval_scope: false, - web: false, + ..RuntimeFeatures::none() }) .iter() .any(|lib| lib == "elephc_crypto")); diff --git a/src/codegen_support/stream_filters/bzip2.rs b/src/codegen_support/stream_filters/bzip2.rs index 240358f22e..b637e01de3 100644 --- a/src/codegen_support/stream_filters/bzip2.rs +++ b/src/codegen_support/stream_filters/bzip2.rs @@ -30,6 +30,7 @@ use crate::codegen_support::abi; use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Platform; /// Size of the libbz2 `bz_stream` struct on LP64 targets, in bytes. const BZ_STREAM_SIZE: i64 = 80; @@ -96,7 +97,7 @@ pub(crate) fn emit_compress_arm64( 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 + 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 @@ -145,7 +146,7 @@ pub(crate) fn emit_compress_arm64( 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.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 @@ -163,7 +164,7 @@ pub(crate) fn emit_compress_arm64( // -- 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.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 @@ -199,7 +200,7 @@ pub(crate) fn emit_compress_arm64( 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 + 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 @@ -254,8 +255,10 @@ pub(crate) fn emit_compress_x86_64( 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 -- + emit_x86_stream_slot(emitter, "r11"); + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // restore payload length clobbered by the Windows slot registry 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 r10, QWORD PTR [r9 + r11*8]"); // r10 = bz_stream pointer for this compact/legacy descriptor slot 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 @@ -268,7 +271,7 @@ pub(crate) fn emit_compress_x86_64( 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 + emitter.emit_call_c("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 @@ -297,8 +300,9 @@ pub(crate) fn emit_compress_x86_64( 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 + emit_x86_stream_slot(emitter, "r11"); 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 r10, QWORD PTR [r9 + r11*8]"); // r10 = bz_stream pointer for this compact/legacy descriptor slot 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 @@ -314,7 +318,7 @@ pub(crate) fn emit_compress_x86_64( 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.emit_call_c("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 @@ -329,10 +333,11 @@ pub(crate) fn emit_compress_x86_64( // -- 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.emit_call_c("BZ2_bzCompressEnd"); // release libbz2's internal compress state emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the file descriptor + emit_x86_stream_slot(emitter, "r11"); 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.instruction("mov QWORD PTR [r9 + r11*8], 0"); // clear this compact/legacy descriptor slot'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 @@ -371,15 +376,16 @@ pub(crate) fn emit_compress_x86_64( 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 + emitter.emit_call_c("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 + emit_x86_stream_slot(emitter, "r11"); + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the bz_stream pointer after the Windows slot lookup 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 + emitter.instruction("mov QWORD PTR [r9 + r11*8], r10"); // store the bz_stream handle for this compact/legacy descriptor slot 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 + emitter.instruction("mov BYTE PTR [r9 + r11], 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 @@ -397,3 +403,74 @@ pub(crate) fn emit_compress_x86_64( 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 } + +/// Emits the x86_64 compact stream slot used by bzip2 filter state tables. +fn emit_x86_stream_slot(emitter: &mut Emitter, slot_reg: &str) { + if emitter.target.platform == Platform::Windows { + emitter.instruction("call __rt_win_stream_slot"); // map raw Windows descriptor to a bounded stream-state slot + emitter.instruction(&format!("mov {}, rax", slot_reg)); // retain compact slot for table access + } else { + emitter.instruction(&format!("mov {}, rdi", slot_reg)); // preserve Linux descriptor indexing + } +} + +#[cfg(test)] +mod tests { + //! Purpose: + //! Regression tests for Windows bzip2 stream-filter assembly emission. + //! + //! Called from: + //! - `cargo test` through Rust's test harness. + //! + //! Key details: + //! - The Windows compact-slot registry uses `rdx` as scratch, so the + //! compressor must restore its saved PHP payload length before seeding + //! `bz_stream.avail_in`. + + use crate::codegen_support::emit::Emitter; + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::emit_compress_x86_64; + + /// Verifies the Windows bzip2 write helper restores its saved payload + /// length after compact-slot lookup before initializing `avail_in`. + #[test] + fn windows_bzip2_write_restores_length_after_stream_slot_lookup() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_compress_x86_64(&mut emitter, "bz_write", "bz_close", "after_bz", 9, 0); + let asm = emitter.output(); + let slot_lookup = asm + .find("call __rt_win_stream_slot") + .expect("Windows bzip2 helper must use a compact stream slot"); + let restore = asm[slot_lookup..] + .find("mov rdx, QWORD PTR [rbp - 16]") + .map(|offset| slot_lookup + offset) + .expect("slot lookup must not leak its rdx scratch value into bz_stream.avail_in"); + let seed = asm[restore..] + .find("mov DWORD PTR [r10 + 8], edx") + .map(|offset| restore + offset) + .expect("bzip2 helper must seed avail_in from the restored length"); + assert!(slot_lookup < restore && restore < seed); + } + + /// Verifies Windows reloads the allocated bzip2 stream after compact-slot + /// lookup before publishing the per-descriptor handle. + #[test] + fn windows_bzip2_attach_restores_handle_after_stream_slot_lookup() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_compress_x86_64(&mut emitter, "bz_write", "bz_close", "after_bz", 9, 0); + let asm = emitter.output(); + let slot_lookup = asm + .rfind("call __rt_win_stream_slot") + .expect("Windows bzip2 attach must use a compact stream slot"); + let restore = asm[slot_lookup..] + .find("mov r10, QWORD PTR [rbp - 16]") + .map(|offset| slot_lookup + offset) + .expect("slot lookup must not replace the allocated bzip2 stream"); + let store = asm[restore..] + .find("mov QWORD PTR [r9 + r11*8], r10") + .map(|offset| restore + offset) + .expect("bzip2 attach must publish the restored stream handle"); + assert!(slot_lookup < restore && restore < store); + } +} diff --git a/src/codegen_support/stream_filters/compress_bzip2_stream.rs b/src/codegen_support/stream_filters/compress_bzip2_stream.rs index b3ea839735..2cbd8e36f5 100644 --- a/src/codegen_support/stream_filters/compress_bzip2_stream.rs +++ b/src/codegen_support/stream_filters/compress_bzip2_stream.rs @@ -86,7 +86,7 @@ where 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.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 @@ -126,7 +126,7 @@ where // 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.bl_c("dup2"); // libc dup2 emitter.instruction("ldr x0, [sp, #32]"); // close temp fd emitter.syscall(6); // close @@ -193,7 +193,7 @@ where 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.emit_call_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 @@ -230,7 +230,7 @@ where 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.emit_call_c("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 diff --git a/src/codegen_support/stream_filters/iconv.rs b/src/codegen_support/stream_filters/iconv.rs index 2436181896..122e33d1fe 100644 --- a/src/codegen_support/stream_filters/iconv.rs +++ b/src/codegen_support/stream_filters/iconv.rs @@ -14,6 +14,12 @@ //! 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. +//! On windows-x86_64 the x86_64 call sites route through `Emitter::emit_call_c` +//! instead of `bl_c`: libiconv IS statically linked there (`src/linker.rs` +//! `-liconv`, MinGW sysroot), MSx64 ABI, so the three symbols get real +//! `__rt_sys_iconv_open`/`__rt_sys_iconv`/`__rt_sys_iconv_close` arg-shuffle +//! shims (`codegen_support::runtime::win32::emit_shim_iconv`) rather than +//! loud-fail stubs. The AArch64 body below is unaffected (still `bl_c`). //! - 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 @@ -90,7 +96,7 @@ pub(crate) fn emit_read_arm64( // -- 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.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 @@ -111,13 +117,13 @@ pub(crate) fn emit_read_arm64( 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.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 + 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 @@ -152,7 +158,7 @@ pub(crate) fn emit_read_arm64( // -- 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 + 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 @@ -226,7 +232,7 @@ pub(crate) fn emit_read_x86_64( // -- 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.emit_call_c("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 @@ -247,12 +253,12 @@ pub(crate) fn emit_read_x86_64( 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.emit_call_c("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 + emitter.emit_call_c("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 @@ -288,7 +294,7 @@ pub(crate) fn emit_read_x86_64( // -- 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 + emitter.emit_call_c("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 diff --git a/src/codegen_support/stream_filters/iconv_write.rs b/src/codegen_support/stream_filters/iconv_write.rs index d1de78060b..69a8f257e0 100644 --- a/src/codegen_support/stream_filters/iconv_write.rs +++ b/src/codegen_support/stream_filters/iconv_write.rs @@ -22,12 +22,18 @@ //! 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 x86_64 `iconv`/`iconv_open`/`iconv_close` call sites route through +//! `Emitter::emit_call_c` (not `bl_c`): on windows-x86_64 this reaches real +//! `__rt_sys_iconv*` MSx64 arg-shuffle shims (libiconv is statically linked +//! there — `src/linker.rs` `-liconv`), byte-identical elsewhere. The AArch64 +//! body is unaffected (still `bl_c`). //! - 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_support::abi; use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Platform; use crate::codegen_support::platform::Arch; /// Capacity of the shared `_stream_grow_scratch` window used as the iconv output buffer. @@ -114,7 +120,7 @@ fn emit_arm64( 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 + 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 @@ -147,7 +153,7 @@ fn emit_arm64( 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.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 @@ -165,7 +171,7 @@ fn emit_arm64( 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.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 @@ -228,13 +234,14 @@ fn emit_x86_64( 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 + emit_x86_stream_slot(emitter, "r11"); 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("mov rdi, QWORD PTR [r9 + r11*8]"); // arg 0 = the iconv_t for this compact/legacy descriptor slot 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 + emitter.emit_call_c("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 @@ -264,16 +271,18 @@ fn emit_x86_64( 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 + emit_x86_stream_slot(emitter, "r11"); 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("mov rsi, QWORD PTR [r9 + r11*8]"); // load this compact/legacy descriptor slot'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.emit_call_c("iconv_close"); // release the iconv descriptor emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the descriptor + emit_x86_stream_slot(emitter, "r11"); 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.instruction("mov QWORD PTR [r9 + r11*8], 0"); // clear this compact/legacy descriptor slot'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 @@ -289,14 +298,17 @@ fn emit_x86_64( 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.emit_call_c("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 QWORD PTR [rbp - 16], rax"); // preserve iconv_t before the Windows slot registry reuses rax emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the file descriptor + emit_x86_stream_slot(emitter, "r11"); 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 + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // restore iconv_t after the compact-slot lookup + emitter.instruction("mov QWORD PTR [r9 + r11*8], r10"); // store iconv_t for this compact/legacy descriptor slot 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("mov BYTE PTR [r9 + r11], 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 @@ -311,3 +323,65 @@ fn emit_x86_64( 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 } + +/// Emits the x86_64 compact stream slot used by iconv filter state tables. +fn emit_x86_stream_slot(emitter: &mut Emitter, slot_reg: &str) { + if emitter.target.platform == Platform::Windows { + emitter.instruction("call __rt_win_stream_slot"); // map raw Windows descriptor to a bounded stream-state slot + emitter.instruction(&format!("mov {}, rax", slot_reg)); // retain compact slot for table access + } else { + emitter.instruction(&format!("mov {}, rdi", slot_reg)); // preserve Linux descriptor indexing + } +} + +#[cfg(test)] +mod tests { + //! Purpose: + //! Regression tests for Windows iconv write-filter assembly emission. + //! + //! Called from: + //! - `cargo test` through Rust's test harness. + //! + //! Key details: + //! - Compact stream-slot lookup returns through `rax`, so it must not + //! overwrite the `iconv_open` result before it is stored in the table. + + use crate::codegen_support::emit::Emitter; + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::emit_iconv_write_attach_with_labels; + + /// Verifies Windows preserves the `iconv_open` handle over compact-slot + /// lookup and stores that handle instead of the lookup's numeric result. + #[test] + fn windows_iconv_write_preserves_handle_across_stream_slot_lookup() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + let mut sequence = 0usize; + emit_iconv_write_attach_with_labels( + &mut emitter, + "from_encoding", + "to_encoding", + |_| { + sequence += 1; + format!("iconv_test_{sequence}") + }, + ); + let asm = emitter.output(); + let save_handle = asm + .find("mov QWORD PTR [rbp - 16], rax") + .expect("iconv_open result must be saved before slot lookup"); + let slot_lookup = asm[save_handle..] + .find("call __rt_win_stream_slot") + .map(|offset| save_handle + offset) + .expect("Windows iconv filter must use a compact stream slot"); + let restore_handle = asm[slot_lookup..] + .find("mov r10, QWORD PTR [rbp - 16]") + .map(|offset| slot_lookup + offset) + .expect("iconv handle must survive slot lookup"); + let store_handle = asm[restore_handle..] + .find("mov QWORD PTR [r9 + r11*8], r10") + .map(|offset| restore_handle + offset) + .expect("iconv table must store the preserved handle"); + assert!(save_handle < slot_lookup && slot_lookup < restore_handle && restore_handle < store_handle); + } +} diff --git a/src/codegen_support/stream_filters/inflate.rs b/src/codegen_support/stream_filters/inflate.rs index daab17e2ba..9a73eae48a 100644 --- a/src/codegen_support/stream_filters/inflate.rs +++ b/src/codegen_support/stream_filters/inflate.rs @@ -88,7 +88,7 @@ where 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 + 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"); @@ -103,11 +103,11 @@ where // -- 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.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 + 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 @@ -142,7 +142,7 @@ where // -- 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 + 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 @@ -163,6 +163,7 @@ pub(crate) fn emit_x86_64(emitter: &mut Emitter, mut next_label: F) where F: FnMut(&str) -> String, { + let zstream = super::zlib::z_stream_layout(emitter.target); let slurp = next_label("zlib_inflate_slurp"); let slurp_done = next_label("zlib_inflate_slurped"); let sized = next_label("zlib_inflate_sized"); @@ -210,10 +211,10 @@ where 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 -- + // -- zero the target-layout 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!("cmp r9, {}", zstream.size)); // cleared the whole target-layout 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 @@ -224,8 +225,8 @@ where 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 + emitter.instruction(&format!("mov ecx, {}", zstream.size)); // arg 3 = target sizeof(z_stream) for the ABI check + emitter.emit_call_c("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 @@ -233,18 +234,22 @@ where 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(&format!("mov QWORD PTR [rsp + {}], r9", zstream.next_out)); // 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 + emitter.instruction(&format!("mov DWORD PTR [rsp + {}], r9d", zstream.avail_out)); // 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.emit_call_c("inflate"); // decompress the entire input at once + if emitter.target.platform == crate::codegen_support::platform::Platform::Windows { + emitter.instruction(&format!("mov eax, DWORD PTR [rsp + {}]", zstream.total_out)); // LLP64 z_stream.total_out = decompressed length + } else { + emitter.instruction(&format!("mov rax, QWORD PTR [rsp + {}]", zstream.total_out)); // LP64 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 + emitter.emit_call_c("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 @@ -280,7 +285,7 @@ where // -- 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 + emitter.emit_call_c("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 diff --git a/src/codegen_support/stream_filters/zlib.rs b/src/codegen_support/stream_filters/zlib.rs index 7958e635e6..05374bd6b2 100644 --- a/src/codegen_support/stream_filters/zlib.rs +++ b/src/codegen_support/stream_filters/zlib.rs @@ -16,15 +16,42 @@ //! `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 +//! - `z_stream` is target-layout-sensitive: 112 bytes on LP64 and 88 bytes on +//! Windows LLP64, where `unsigned long` is 32-bit. 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_support::abi; use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::{Platform, Target}; -/// Size of the libz `z_stream` struct on LP64 targets, in bytes. -const Z_STREAM_SIZE: i64 = 112; +/// Target C layout for the `z_stream` fields emitted code reads or writes. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct ZStreamLayout { + pub(crate) size: i64, + pub(crate) next_out: i64, + pub(crate) avail_out: i64, + pub(crate) total_out: i64, +} + +/// Returns zlib's C `z_stream` layout for the target data model. +pub(crate) fn z_stream_layout(target: Target) -> ZStreamLayout { + if target.platform == Platform::Windows { + ZStreamLayout { + size: 88, + next_out: 16, + avail_out: 24, + total_out: 28, + } + } else { + ZStreamLayout { + size: 112, + next_out: 24, + avail_out: 32, + total_out: 40, + } + } +} /// Capacity of the shared `_stream_filter_buf` scratch used as the deflate /// output window. const FILTER_BUF_SIZE: i64 = 65536; @@ -37,6 +64,7 @@ pub(crate) fn emit_arm64( skip_label: &str, level: i64, ) { + let zstream = z_stream_layout(emitter.target); // -- 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 @@ -69,7 +97,7 @@ pub(crate) fn emit_arm64( 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 + 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 @@ -118,7 +146,7 @@ pub(crate) fn emit_arm64( 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.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 @@ -136,7 +164,7 @@ pub(crate) fn emit_arm64( // -- 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.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 @@ -151,16 +179,16 @@ pub(crate) fn emit_arm64( 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(&format!("mov x0, #{}", zstream.size)); // request a target-layout 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 -- + // -- zero the target-layout z_stream so zalloc/zfree are NULL -- 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!("cmp x9, #{}", zstream.size)); // cleared the whole target-layout 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 @@ -177,8 +205,8 @@ pub(crate) fn emit_arm64( 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 + emitter.instruction(&format!("mov x7, #{}", zstream.size)); // arg 7 = target 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 @@ -214,6 +242,7 @@ pub(crate) fn emit_x86_64( skip_label: &str, level: i64, ) { + let zstream = z_stream_layout(emitter.target); // -- 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 @@ -230,8 +259,10 @@ pub(crate) fn emit_x86_64( 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 -- + emit_x86_stream_slot(emitter, "r11"); + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // restore payload length clobbered by the Windows slot registry 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 r10, QWORD PTR [r9 + r11*8]"); // r10 = z_stream pointer for this compact/legacy descriptor slot 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 @@ -240,15 +271,15 @@ pub(crate) fn emit_x86_64( 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(&format!("mov QWORD PTR [r10 + {}], r11", zstream.next_out)); // z_stream.next_out = scratch window base + emitter.instruction(&format!("mov DWORD PTR [r10 + {}], {}", zstream.avail_out, 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 + emitter.emit_call_c("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(&format!("sub eax, DWORD PTR [r10 + {}]", zstream.avail_out)); // 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 @@ -257,7 +288,7 @@ pub(crate) fn emit_x86_64( 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!("cmp DWORD PTR [r10 + {}], 0", zstream.avail_out)); // 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 @@ -273,8 +304,9 @@ pub(crate) fn emit_x86_64( 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 + emit_x86_stream_slot(emitter, "r11"); 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 r10, QWORD PTR [r9 + r11*8]"); // r10 = z_stream pointer for this compact/legacy descriptor slot 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 @@ -286,16 +318,16 @@ pub(crate) fn emit_x86_64( 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(&format!("mov QWORD PTR [r10 + {}], r11", zstream.next_out)); // z_stream.next_out = scratch window base + emitter.instruction(&format!("mov DWORD PTR [r10 + {}], {}", zstream.avail_out, 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.emit_call_c("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(&format!("sub eax, DWORD PTR [r10 + {}]", zstream.avail_out)); // 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 @@ -305,10 +337,11 @@ pub(crate) fn emit_x86_64( // -- 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.emit_call_c("deflateEnd"); // release zlib's internal deflate state emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the file descriptor + emit_x86_stream_slot(emitter, "r11"); 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.instruction("mov QWORD PTR [r9 + r11*8], 0"); // clear this compact/legacy descriptor slot'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 @@ -322,7 +355,7 @@ pub(crate) fn emit_x86_64( 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(&format!("mov rax, {}", zstream.size)); // request a target-layout 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 @@ -332,10 +365,10 @@ pub(crate) fn emit_x86_64( 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 -- + // -- zero the target-layout z_stream so zalloc/zfree are NULL -- 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!("cmp r9, {}", zstream.size)); // cleared the whole target-layout 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 @@ -354,17 +387,18 @@ pub(crate) fn emit_x86_64( 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(&format!("mov QWORD PTR [rsp + 8], {}", zstream.size)); // stack arg 7 = target sizeof(z_stream) + emitter.emit_call_c("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 + emit_x86_stream_slot(emitter, "r11"); + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the z_stream pointer after the Windows slot lookup 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 + emitter.instruction("mov QWORD PTR [r9 + r11*8], r10"); // store the z_stream handle for this compact/legacy descriptor slot 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 + emitter.instruction("mov BYTE PTR [r9 + r11], 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 @@ -382,3 +416,110 @@ pub(crate) fn emit_x86_64( 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 } + +/// Emits the x86_64 slot index used by per-stream filter state tables. +/// +/// Windows descriptors may be opaque SOCKET values; only Windows maps them +/// through the bounded runtime registry. Linux retains its established direct +/// descriptor indexing in the generated assembly. +fn emit_x86_stream_slot(emitter: &mut Emitter, slot_reg: &str) { + if emitter.target.platform == Platform::Windows { + emitter.instruction("call __rt_win_stream_slot"); // map raw Windows descriptor to a bounded stream-state slot + emitter.instruction(&format!("mov {}, rax", slot_reg)); // retain compact slot for the following table access + } else { + emitter.instruction(&format!("mov {}, rdi", slot_reg)); // Linux keeps the established descriptor table index + } +} + +#[cfg(test)] +mod tests { + //! Purpose: + //! Regression tests for target-specific zlib `z_stream` C layouts. + //! + //! Called from: + //! - `cargo test` through Rust's test harness. + //! + //! Key details: + //! - Windows uses LLP64, while macOS/Linux use LP64. + + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::{emit_x86_64, z_stream_layout, ZStreamLayout}; + use crate::codegen_support::emit::Emitter; + + /// Verifies MinGW's LLP64 layout uses 32-bit `unsigned long` fields. + #[test] + fn windows_z_stream_layout_matches_mingw_zlib() { + assert_eq!( + z_stream_layout(Target::new(Platform::Windows, Arch::X86_64)), + ZStreamLayout { + size: 88, + next_out: 16, + avail_out: 24, + total_out: 28, + } + ); + } + + /// Verifies Linux keeps zlib's established LP64 layout. + #[test] + fn linux_z_stream_layout_remains_lp64() { + assert_eq!( + z_stream_layout(Target::new(Platform::Linux, Arch::X86_64)), + ZStreamLayout { + size: 112, + next_out: 24, + avail_out: 32, + total_out: 40, + } + ); + } + + /// Verifies the Windows deflate write helper restores its saved payload + /// length after compact-slot lookup, whose registry uses `rdx` as scratch. + #[test] + fn windows_deflate_write_restores_length_after_stream_slot_lookup() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_x86_64(&mut emitter, "deflate_write", "deflate_close", "after_deflate", -1); + let asm = emitter.output(); + let slot_lookup = asm + .find("call __rt_win_stream_slot") + .expect("Windows deflate helper must use a compact stream slot"); + let restore = asm[slot_lookup..] + .find("mov rdx, QWORD PTR [rbp - 16]") + .map(|offset| slot_lookup + offset) + .expect("slot lookup must not leak its rdx scratch value into z_stream.avail_in"); + let seed = asm[restore..] + .find("mov DWORD PTR [r10 + 8], edx") + .map(|offset| restore + offset) + .expect("deflate helper must seed avail_in from the restored length"); + assert!(slot_lookup < restore && restore < seed); + } + + /// Verifies Windows reloads the allocated zlib stream after compact-slot + /// lookup before publishing the per-descriptor handle. + #[test] + fn windows_deflate_attach_restores_handle_after_stream_slot_lookup() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_x86_64( + &mut emitter, + "deflate_write", + "deflate_close", + "after_deflate", + -1, + ); + let asm = emitter.output(); + let slot_lookup = asm + .rfind("call __rt_win_stream_slot") + .expect("Windows deflate attach must use a compact stream slot"); + let restore = asm[slot_lookup..] + .find("mov r10, QWORD PTR [rbp - 16]") + .map(|offset| slot_lookup + offset) + .expect("slot lookup must not replace the allocated zlib stream"); + let store = asm[restore..] + .find("mov QWORD PTR [r9 + r11*8], r10") + .map(|offset| restore + offset) + .expect("deflate attach must publish the restored stream handle"); + assert!(slot_lookup < restore && restore < store); + } +} diff --git a/src/codegen_support/tls.rs b/src/codegen_support/tls.rs index aff644b4b5..ac5badab88 100644 --- a/src/codegen_support/tls.rs +++ b/src/codegen_support/tls.rs @@ -11,12 +11,16 @@ //! reference the elephc-tls staticlib entry points during linking. use crate::codegen_support::emit::Emitter; -use crate::codegen_support::{abi, platform::Arch}; +use crate::codegen_support::{abi, platform::{Arch, Platform}}; /// Stores elephc-tls C entry point addresses in the runtime indirection slots. pub(crate) fn publish_tls_function_pointers(emitter: &mut Emitter) { const ENTRIES: &[(&str, &str)] = &[ ("elephc_tls_connect", "_elephc_tls_connect_fn"), + ( + "elephc_tls_connect_with_options", + "_elephc_tls_connect_with_options_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"), @@ -24,7 +28,12 @@ pub(crate) fn publish_tls_function_pointers(emitter: &mut Emitter) { ("elephc_tls_write", "_elephc_tls_write_fn"), ("elephc_tls_read", "_elephc_tls_read_fn"), ("elephc_tls_close", "_elephc_tls_close_fn"), + ("elephc_tls_handshake", "_elephc_tls_handshake_fn"), ("elephc_tls_attach_fd", "_elephc_tls_attach_fd_fn"), + ( + "elephc_tls_attach_fd_with_options", + "_elephc_tls_attach_fd_with_options_fn", + ), ( "elephc_tls_attach_fd_client_cert", "_elephc_tls_attach_fd_client_cert_fn", @@ -45,10 +54,207 @@ pub(crate) fn publish_tls_function_pointers(emitter: &mut Emitter) { } 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); + if emitter.platform == Platform::Windows { + abi::emit_symbol_address(emitter, "r9", &format!("__rt_tls_abi_{c_name}")); + } else { + 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 } } } } + +/// Emits Windows x86_64 adapters that translate the compiler's internal SysV +/// register convention to the MS x64 ABI used by Rust `extern "C"` exports. +pub(crate) fn emit_tls_abi_adapters(emitter: &mut Emitter) { + if emitter.platform != Platform::Windows || emitter.target.arch != Arch::X86_64 { + return; + } + + for name in [ + "elephc_tls_connect", + "elephc_tls_connect_insecure", + "elephc_tls_write", + "elephc_tls_read", + "elephc_tls_attach_fd", + ] { + emit_adapter_3(emitter, name); + } + emit_adapter_5(emitter, "elephc_tls_attach_fd_with_options"); + emit_adapter_4(emitter, "elephc_tls_connect_with_options"); + emit_adapter_1(emitter, "elephc_tls_handshake"); + for name in [ + "elephc_tls_connect_cafile", + "elephc_tls_connect_capath", + "elephc_tls_connect_peer_name", + ] { + emit_adapter_5(emitter, name); + } + for name in [ + "elephc_tls_attach_fd_client_cert", + "elephc_tls_connect_client_cert", + ] { + emit_adapter_7(emitter, name); + } + emit_adapter_1(emitter, "elephc_tls_close"); +} + +/// Emits one one-argument SysV-to-MS x64 TLS export adapter. +fn emit_adapter_1(emitter: &mut Emitter, name: &str) { + emitter.label_global(&format!("__rt_tls_abi_{name}")); + emitter.instruction("sub rsp, 40"); // reserve MS x64 shadow space and preserve call alignment + emitter.instruction("mov rcx, rdi"); // translate positional argument one into the MS x64 register + emitter.instruction(&format!("call {}", emitter.target.extern_symbol(name))); // invoke the Rust extern C export with the native Windows ABI + emitter.instruction("add rsp, 40"); // release shadow space and restore the internal stack + emitter.instruction("ret"); // return the bridge result unchanged + emitter.blank(); +} + +/// Emits one three-argument SysV-to-MS x64 TLS export adapter. +fn emit_adapter_3(emitter: &mut Emitter, name: &str) { + emitter.label_global(&format!("__rt_tls_abi_{name}")); + emitter.instruction("sub rsp, 40"); // reserve MS x64 shadow space and preserve call alignment + emitter.instruction("mov r8, rdx"); // preserve and translate positional argument three first + emitter.instruction("mov rdx, rsi"); // translate positional argument two + emitter.instruction("mov rcx, rdi"); // translate positional argument one + emitter.instruction(&format!("call {}", emitter.target.extern_symbol(name))); // invoke the Rust extern C export with the native Windows ABI + emitter.instruction("add rsp, 40"); // release shadow space and restore the internal stack + emitter.instruction("ret"); // return the bridge result unchanged + emitter.blank(); +} + +/// Emits one four-argument SysV-to-MS x64 TLS export adapter. +fn emit_adapter_4(emitter: &mut Emitter, name: &str) { + emitter.label_global(&format!("__rt_tls_abi_{name}")); + emitter.instruction("sub rsp, 40"); // reserve MS x64 shadow space and preserve call alignment + emitter.instruction("mov r9, rcx"); // preserve and translate positional argument four first + emitter.instruction("mov r8, rdx"); // translate positional argument three + emitter.instruction("mov rdx, rsi"); // translate positional argument two + emitter.instruction("mov rcx, rdi"); // translate positional argument one + emitter.instruction(&format!("call {}", emitter.target.extern_symbol(name))); // invoke the Rust extern C export with the native Windows ABI + emitter.instruction("add rsp, 40"); // release shadow space and restore the internal stack + emitter.instruction("ret"); // return the bridge result unchanged + emitter.blank(); +} + +/// Emits one five-argument SysV-to-MS x64 TLS export adapter. +fn emit_adapter_5(emitter: &mut Emitter, name: &str) { + emitter.label_global(&format!("__rt_tls_abi_{name}")); + emitter.instruction("sub rsp, 56"); // reserve shadow, one stack argument, and alignment padding + emitter.instruction("mov r10, rcx"); // preserve SysV positional argument four before overwriting rcx + emitter.instruction("mov r11, r8"); // preserve SysV positional argument five before overwriting r8 + emitter.instruction("mov r8, rdx"); // translate positional argument three + emitter.instruction("mov rdx, rsi"); // translate positional argument two + emitter.instruction("mov rcx, rdi"); // translate positional argument one + emitter.instruction("mov r9, r10"); // translate positional argument four + emitter.instruction("mov QWORD PTR [rsp + 32], r11"); // place positional argument five above MS shadow space + emitter.instruction(&format!("call {}", emitter.target.extern_symbol(name))); // invoke the Rust extern C export with the native Windows ABI + emitter.instruction("add rsp, 56"); // release shadow, stack argument, and padding + emitter.instruction("ret"); // return the bridge result unchanged + emitter.blank(); +} + +/// Emits one seven-argument SysV-to-MS x64 TLS export adapter. +fn emit_adapter_7(emitter: &mut Emitter, name: &str) { + emitter.label_global(&format!("__rt_tls_abi_{name}")); + emitter.instruction("sub rsp, 72"); // reserve shadow plus three MS stack arguments and preserve alignment + emitter.instruction("mov r10, rcx"); // preserve SysV positional argument four before overwriting rcx + emitter.instruction("mov r11, r8"); // preserve SysV positional argument five before overwriting r8 + emitter.instruction("mov rax, r9"); // preserve SysV positional argument six before overwriting r9 + emitter.instruction("mov r8, rdx"); // translate positional argument three + emitter.instruction("mov rdx, rsi"); // translate positional argument two + emitter.instruction("mov rcx, rdi"); // translate positional argument one + emitter.instruction("mov r9, r10"); // translate positional argument four + emitter.instruction("mov QWORD PTR [rsp + 32], r11"); // place positional argument five above MS shadow space + emitter.instruction("mov QWORD PTR [rsp + 40], rax"); // place positional argument six in the next MS stack slot + emitter.instruction("mov r10, QWORD PTR [rsp + 80]"); // load SysV positional argument seven from above the adapter frame + emitter.instruction("mov QWORD PTR [rsp + 48], r10"); // place positional argument seven in the third MS stack slot + emitter.instruction(&format!("call {}", emitter.target.extern_symbol(name))); // invoke the Rust extern C export with the native Windows ABI + emitter.instruction("add rsp, 72"); // release shadow and the three stack arguments + emitter.instruction("ret"); // return the bridge result unchanged + emitter.blank(); +} + +#[cfg(test)] +mod tests { + use std::fs; + use std::path::Path; + + use crate::codegen_support::platform::Target; + + use super::*; + + /// Verifies Windows publishing selects ABI adapters and a seven-argument + /// adapter retrieves SysV argument seven before filling MS x64 stack slots. + #[test] + fn windows_tls_exports_use_ms_x64_adapters() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_tls_abi_adapters(&mut emitter); + publish_tls_function_pointers(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("__rt_tls_abi_elephc_tls_attach_fd_client_cert:")); + assert!(asm.contains("__rt_tls_abi_elephc_tls_attach_fd_with_options:")); + assert!(asm.contains("__rt_tls_abi_elephc_tls_connect_with_options:")); + assert!(asm.contains("__rt_tls_abi_elephc_tls_handshake:")); + assert!(asm.contains("lea r9, [rip + __rt_tls_abi_elephc_tls_connect_with_options]")); + assert!(asm.contains("mov r10, QWORD PTR [rsp + 80]")); + assert!(asm.contains("mov QWORD PTR [rsp + 48], r10")); + assert!(asm.contains("lea r9, [rip + __rt_tls_abi_elephc_tls_attach_fd]")); + } + + /// Guards the STARTTLS lowering contract that distinguishes explicit + /// crypto method zero from null/omission and forwards a reusable source + /// session through both target ABIs. + #[test] + fn stream_crypto_lowering_forwards_method_presence_and_source_session() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let source = + fs::read_to_string(root.join("src/codegen/lower_inst/builtins/io.rs")) + .expect("read stream crypto lowering"); + for fragment in [ + "str x9, [sp, #32]", + "mov QWORD PTR [rsp + 32], 1", + "str x0, [sp, #40]", + "mov QWORD PTR [rsp + 40], rax", + "ldr x3, [sp, #128]", + "ldr x4, [sp, #136]", + "mov rcx, QWORD PTR [rsp + 128]", + "mov r8, QWORD PTR [rsp + 136]", + ] { + assert!( + source.contains(fragment), + "missing STARTTLS ABI fragment: {fragment}" + ); + } + } + + /// Guards every generated-assembly TLS slot consumer so published pointers + /// stay explicitly classified as compiler-runtime ABI entries. On Windows + /// these slots contain SysV-to-MSx64 adapters and must not be remapped again. + #[test] + fn tls_slot_consumers_use_published_bridge_calls() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + for (relative, expected_calls) in [ + ("src/codegen/lower_inst/builtins/io.rs", 8usize), + ( + "src/codegen_support/runtime/io/file_get_contents_url.rs", + 2, + ), + ("src/codegen_support/runtime/io/fread.rs", 2), + ("src/codegen_support/runtime/io/ftp.rs", 8), + ("src/codegen_support/runtime/io/fwrite.rs", 2), + ("src/codegen_support/runtime/io/https.rs", 8), + ("src/codegen_support/runtime/io/tls_session_table.rs", 2), + ] { + let source = fs::read_to_string(root.join(relative)) + .expect("TLS slot consumer source must be readable"); + assert_eq!( + source.matches("emit_published_bridge_call").count(), + expected_calls, + "{relative} must classify every TLS slot call through emit_published_bridge_call" + ); + } + } +} diff --git a/src/codegen_support/tz_bridge.rs b/src/codegen_support/tz_bridge.rs new file mode 100644 index 0000000000..dd3253b693 --- /dev/null +++ b/src/codegen_support/tz_bridge.rs @@ -0,0 +1,51 @@ +//! Purpose: +//! Call-site support for routing windows-x86_64 IANA-offset resolution through +//! the elephc-tz staticlib. Publishes the offset and abbreviation C entry points +//! into runtime function-pointer slots immediately before a date/mktime/strtotime +//! call site whose target is windows-x86_64, mirroring +//! `hash_crypto::publish_elephc_crypto_function_pointers`. +//! +//! Called from: +//! - `crate::codegen::lower_inst::builtins::system` date/gmdate, mktime/gmmktime, +//! and strtotime lowerers (each publishes the fn pointer immediately before its +//! `__rt_date`/`__rt_mktime`/`__rt_strtotime` call, windows-x86_64 only). +//! +//! Key details: +//! - The fn pointer is published indirectly (mirroring the `_elephc_crypto_hash_fn` +//! pattern) so only windows programs that actually call a date/time builtin +//! reference the elephc-tz offset entry point and therefore pull in +//! `-lelephc_tz` at link time (pay-for-use). The shared `__rt_sys_localtime` / +//! `__rt_sys_mktime` Win32 shims that consume the slot +//! (`codegen_support/runtime/win32/shims_time.rs`) gracefully fall back to raw +//! msvcrt `localtime()`/`mktime()` when the slot is unpublished (null) — +//! exactly today's behavior — so a missed call site degrades, it never breaks. +//! - x86_64-only: elephc's Windows PE target is x86_64-only (see +//! `Emitter::entry_symbol`'s `panic!` for Windows ARM64), so unlike +//! `hash_crypto` (which targets every platform/arch) there is no AArch64 +//! branch to mirror. + +use crate::codegen_support::abi; +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::{Arch, Platform}; + +/// Publishes the elephc-tz offset and abbreviation entry points into their +/// runtime slots so the Windows time shims can synthesize complete `struct tm` +/// timezone fields from the baked IANA transition tables. +/// +/// Callers must guard this with a `target.platform == Platform::Windows` check +/// before emitting it (mirroring every other late-bound Windows-only entry +/// point): emitting it unconditionally would make every target reference the +/// elephc-tz staticlib, defeating pay-for-use linking. +pub(crate) fn publish_elephc_tz_offset_function_pointer(emitter: &mut Emitter) { + debug_assert_eq!( + (emitter.target.platform, emitter.target.arch), + (Platform::Windows, Arch::X86_64), + "the elephc-tz offset fn pointer is windows-x86_64 only" + ); + let extern_sym = emitter.target.extern_symbol("elephc_tz_offset"); + abi::emit_extern_symbol_address(emitter, "r9", &extern_sym); + abi::emit_store_reg_to_symbol(emitter, "r9", "_elephc_tz_offset_fn", 0); // publish the elephc-tz offset entry into its runtime slot + let abbreviation_sym = emitter.target.extern_symbol("elephc_tz_abbreviation"); + abi::emit_extern_symbol_address(emitter, "r9", &abbreviation_sym); + abi::emit_store_reg_to_symbol(emitter, "r9", "_elephc_tz_abbreviation_fn", 0); // publish stable transition-abbreviation lookup +} diff --git a/src/codegen_support/wrappers/callback.rs b/src/codegen_support/wrappers/callback.rs index 7ce8f42b13..0d0a0aa3e7 100644 --- a/src/codegen_support/wrappers/callback.rs +++ b/src/codegen_support/wrappers/callback.rs @@ -10,7 +10,7 @@ use crate::codegen_support::abi; use crate::codegen_support::emit::Emitter; -use crate::codegen_support::platform::Arch; +use crate::codegen_support::platform::{Arch, Platform}; use crate::types::PhpType; mod descriptor; @@ -49,7 +49,7 @@ pub(crate) fn emit_callback_wrapper(emitter: &mut Emitter, wrapper: &DeferredCal 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_visible_args(emitter, &wrapper.visible_arg_types, false); spill_captures( emitter, wrapper.visible_arg_types.len(), @@ -64,7 +64,10 @@ pub(crate) fn emit_callback_wrapper(emitter: &mut Emitter, wrapper: &DeferredCal &wrapper.capture_types, frame_size, ); + let call_pad_bytes = abi::outgoing_call_stack_pad_bytes(emitter.target, 0); + abi::emit_reserve_temporary_stack(emitter, call_pad_bytes); abi::emit_call_reg(emitter, "x19"); + abi::emit_release_temporary_stack(emitter, call_pad_bytes); 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 @@ -107,7 +110,7 @@ fn emit_x86_64_callback_wrapper(emitter: &mut Emitter, wrapper: &DeferredCallbac 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_visible_args(emitter, &wrapper.visible_arg_types, false); spill_captures( emitter, wrapper.visible_arg_types.len(), @@ -121,7 +124,10 @@ fn emit_x86_64_callback_wrapper(emitter: &mut Emitter, wrapper: &DeferredCallbac &target_visible_arg_types, &wrapper.capture_types, ); + let call_pad_bytes = abi::outgoing_call_stack_pad_bytes(emitter.target, 0); + abi::emit_reserve_temporary_stack(emitter, call_pad_bytes); abi::emit_call_reg(emitter, "r12"); + abi::emit_release_temporary_stack(emitter, call_pad_bytes); 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); @@ -170,15 +176,73 @@ fn incoming_env_reg(emitter: &Emitter, visible_arg_types: &[PhpType]) -> &'stati /// 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]) { +fn spill_visible_args( + emitter: &mut Emitter, + visible_arg_types: &[PhpType], + native_c_abi: bool, +) { 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); + let assignments = if native_c_abi { + abi::build_c_abi_outgoing_arg_assignments_for_target(emitter.target, &visible_types) + } else { + abi::build_outgoing_arg_assignments_for_target(emitter.target, &visible_types, 0) + }; + let mut stack_offset = if native_c_abi + && (emitter.target.platform, emitter.target.arch) + == (Platform::Windows, Arch::X86_64) + { + 48 + } else { + abi::IncomingArgCursor::for_target(emitter.target, 0).caller_stack_offset + }; for (idx, (ty, assignment)) in visible_types.iter().zip(assignments.iter()).enumerate() { - debug_assert!(assignment.in_register()); + if !assignment.in_register() { + let scalar_scratch = match emitter.target.arch { + Arch::AArch64 => "x9", + Arch::X86_64 => "r10", + }; + let float_scratch = match emitter.target.arch { + Arch::AArch64 => "d15", + Arch::X86_64 => "xmm15", + }; + match ty { + PhpType::Float => { + abi::load_from_caller_stack(emitter, float_scratch, stack_offset); + abi::store_at_offset(emitter, float_scratch, frame_arg_slot_offset(idx)); + } + PhpType::Str => { + let high_scratch = match emitter.target.arch { + Arch::AArch64 => "x10", + Arch::X86_64 => "r11", + }; + abi::load_from_caller_stack(emitter, scalar_scratch, stack_offset); + abi::load_from_caller_stack(emitter, high_scratch, stack_offset + 8); + abi::store_at_offset(emitter, scalar_scratch, frame_arg_slot_offset(idx)); + abi::store_at_offset( + emitter, + high_scratch, + frame_arg_slot_offset(idx) - 8, + ); + } + PhpType::Void | PhpType::Never => {} + _ => { + abi::load_from_caller_stack(emitter, scalar_scratch, stack_offset); + abi::store_at_offset(emitter, scalar_scratch, frame_arg_slot_offset(idx)); + } + } + stack_offset += if native_c_abi + && (emitter.target.platform, emitter.target.arch) + == (Platform::Windows, Arch::X86_64) + { + ty.register_count() * 8 + } else { + 16 + }; + continue; + } match (emitter.target.arch, ty) { (Arch::AArch64, PhpType::Float) => { let reg = abi::float_arg_reg_name(emitter.target, assignment.start_reg); diff --git a/src/codegen_support/wrappers/callback/descriptor.rs b/src/codegen_support/wrappers/callback/descriptor.rs index 1548f2c8a2..b4308a7ecd 100644 --- a/src/codegen_support/wrappers/callback/descriptor.rs +++ b/src/codegen_support/wrappers/callback/descriptor.rs @@ -8,12 +8,13 @@ //! Key details: //! - Runtime array helpers expect callee-saved loop registers to survive callback invocation. //! - Descriptor invokers return boxed `Mixed`; wrappers cast or detach results before returning. +//! - Native MSx64 trampolines preserve the additional nonvolatile registers before entering internal helpers. use crate::codegen_support::abi; use crate::codegen_support::arrays::emit_array_value_type_stamp; use crate::codegen_support::callable_descriptor; use crate::codegen_support::emit::Emitter; -use crate::codegen_support::platform::Arch; +use crate::codegen_support::platform::{Arch, Platform}; use crate::codegen_support::{DeferredCallbackWrapper, DeferredExternCallbackTrampoline}; use crate::types::PhpType; @@ -56,7 +57,7 @@ fn emit_aarch64_descriptor_callback_wrapper( emitter.instruction(&format!("mov x20, {}", env_reg)); // keep the descriptor callback environment pointer across nested calls emitter.instruction("ldr x19, [x20]"); // load the selected callable descriptor from env slot zero - spill_visible_args(emitter, &wrapper.visible_arg_types); + spill_visible_args(emitter, &wrapper.visible_arg_types, false); emit_build_descriptor_invoker_arg_array(emitter, wrapper, frame_size, "x20"); emit_box_descriptor_arg_array_as_mixed(emitter, frame_size, visible_count); emit_call_descriptor_invoker_from_wrapper(emitter, "x19"); @@ -96,7 +97,7 @@ fn emit_x86_64_descriptor_callback_wrapper( emitter.instruction(&format!("mov r13, {}", env_reg)); // keep the descriptor callback environment pointer across nested calls emitter.instruction("mov r12, QWORD PTR [r13]"); // load the selected callable descriptor from env slot zero - spill_visible_args(emitter, &wrapper.visible_arg_types); + spill_visible_args(emitter, &wrapper.visible_arg_types, false); emit_build_descriptor_invoker_arg_array(emitter, wrapper, frame_size, "r13"); emit_box_descriptor_arg_array_as_mixed(emitter, frame_size, visible_count); emit_call_descriptor_invoker_from_wrapper(emitter, "r12"); @@ -146,7 +147,7 @@ fn emit_aarch64_extern_callback_trampoline( emitter.instruction(&format!("stp x21, x22, [sp, #{}]", saved_runtime_offset)); // preserve runtime-loop registers across descriptor invocation abi::emit_load_symbol_to_reg(emitter, "x19", &trampoline.descriptor_slot_label, 0); - spill_visible_args(emitter, &wrapper.visible_arg_types); + spill_visible_args(emitter, &wrapper.visible_arg_types, true); emit_build_descriptor_invoker_arg_array(emitter, &wrapper, frame_size, "x20"); emit_box_descriptor_arg_array_as_mixed(emitter, frame_size, visible_count); emit_call_descriptor_invoker_from_wrapper(emitter, "x19"); @@ -166,11 +167,19 @@ fn emit_x86_64_extern_callback_trampoline( let wrapper = extern_trampoline_wrapper_view(trampoline); let visible_count = wrapper.visible_arg_types.len(); let slot_count = (visible_count + 1).max(1); - let frame_size = align16(slot_count * 16 + 64); + let is_windows_native = emitter.target.platform == Platform::Windows; + let frame_size = if is_windows_native { + align16(slot_count * 16 + 240) + } else { + align16(slot_count * 16 + 64) + }; let saved_descriptor_offset = slot_count * 16 + 16; let saved_env_offset = slot_count * 16 + 24; let saved_runtime_index_offset = slot_count * 16 + 32; let saved_runtime_count_offset = slot_count * 16 + 40; + let saved_rdi_offset = slot_count * 16 + 48; + let saved_rsi_offset = slot_count * 16 + 56; + let saved_xmm_base_offset = slot_count * 16 + 80; emitter.blank(); emitter.comment(&format!( @@ -184,9 +193,21 @@ fn emit_x86_64_extern_callback_trampoline( abi::store_at_offset(emitter, "r13", saved_env_offset); abi::store_at_offset(emitter, "r14", saved_runtime_index_offset); abi::store_at_offset(emitter, "r15", saved_runtime_count_offset); + // -- preserve MSx64 registers not preserved by the internal SysV-compatible ABI -- + if is_windows_native { + abi::store_at_offset(emitter, "rdi", saved_rdi_offset); + abi::store_at_offset(emitter, "rsi", saved_rsi_offset); + for xmm_index in 6..=15 { + let offset = saved_xmm_base_offset + (xmm_index - 6) * 16; + emitter.instruction(&format!( // preserve an MSx64 nonvolatile vector register across SysV helpers + "movdqu XMMWORD PTR [rbp - {}], xmm{}", + offset, xmm_index + )); + } + } abi::emit_load_symbol_to_reg(emitter, "r12", &trampoline.descriptor_slot_label, 0); - spill_visible_args(emitter, &wrapper.visible_arg_types); + spill_visible_args(emitter, &wrapper.visible_arg_types, true); emit_build_descriptor_invoker_arg_array(emitter, &wrapper, frame_size, "r13"); emit_box_descriptor_arg_array_as_mixed(emitter, frame_size, visible_count); emit_call_descriptor_invoker_from_wrapper(emitter, "r12"); @@ -196,6 +217,18 @@ fn emit_x86_64_extern_callback_trampoline( abi::load_at_offset(emitter, "r14", saved_runtime_index_offset); abi::load_at_offset(emitter, "r13", saved_env_offset); abi::load_at_offset(emitter, "r12", saved_descriptor_offset); + // -- restore MSx64 registers after all internal SysV-compatible helper calls -- + if is_windows_native { + for xmm_index in (6..=15).rev() { + let offset = saved_xmm_base_offset + (xmm_index - 6) * 16; + emitter.instruction(&format!( // restore an MSx64 nonvolatile vector register after SysV helpers + "movdqu xmm{}, XMMWORD PTR [rbp - {}]", + xmm_index, offset + )); + } + abi::load_at_offset(emitter, "rsi", saved_rsi_offset); + abi::load_at_offset(emitter, "rdi", saved_rdi_offset); + } abi::emit_frame_restore(emitter, frame_size); abi::emit_return(emitter); } @@ -390,7 +423,10 @@ fn emit_call_descriptor_invoker_from_wrapper(emitter: &mut Emitter, descriptor_r descriptor_arg_reg, ); abi::emit_push_reg(emitter, array_arg_reg); // preserve the boxed argument container for release after descriptor invocation + let call_pad_bytes = abi::outgoing_call_stack_pad_bytes(emitter.target, 0); + abi::emit_reserve_temporary_stack(emitter, call_pad_bytes); abi::emit_call_reg(emitter, invoker_reg); + abi::emit_release_temporary_stack(emitter, call_pad_bytes); emit_release_preserved_mixed_argument_after_result(emitter); } @@ -475,3 +511,95 @@ fn descriptor_array_frame_offset( Arch::X86_64 => frame_arg_slot_offset(visible_count), } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::codegen_support::platform::{Arch, Platform, Target}; + + /// Verifies an extern callback receives mixed MSx64 arguments from their + /// positional registers and native eight-byte overflow slots. + #[test] + fn windows_extern_callback_spills_native_mixed_argument_layout() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + let trampoline = DeferredExternCallbackTrampoline { + label: "ffi_callback".to_string(), + descriptor_slot_label: "ffi_callback_descriptor".to_string(), + visible_arg_types: vec![ + PhpType::Int, + PhpType::Float, + PhpType::Int, + PhpType::Float, + PhpType::Int, + PhpType::Float, + ], + return_type: PhpType::Int, + }; + + emit_x86_64_extern_callback_trampoline(&mut emitter, &trampoline); + let out = emitter.output(); + + assert!(out.contains("mov QWORD PTR [rbp - 16], rcx")); + assert!(out.contains("movsd QWORD PTR [rbp - 32], xmm1")); + assert!(out.contains("mov QWORD PTR [rbp - 48], r8")); + assert!(out.contains("movsd QWORD PTR [rbp - 64], xmm3")); + assert!(out.contains("mov r10, QWORD PTR [rbp + 48]")); + assert!(out.contains("movsd xmm15, QWORD PTR [rbp + 56]")); + } + + /// Verifies a native MSx64 callback preserves every register that the + /// internal SysV-compatible helper convention is otherwise allowed to clobber. + #[test] + fn windows_extern_callback_preserves_msx64_nonvolatile_registers() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + let trampoline = DeferredExternCallbackTrampoline { + label: "ffi_callback".to_string(), + descriptor_slot_label: "ffi_callback_descriptor".to_string(), + visible_arg_types: vec![PhpType::Int], + return_type: PhpType::Float, + }; + + emit_x86_64_extern_callback_trampoline(&mut emitter, &trampoline); + let out = emitter.output(); + + assert!(out.contains("mov QWORD PTR [rbp - 80], rdi"), "{out}"); + assert!(out.contains("mov QWORD PTR [rbp - 88], rsi"), "{out}"); + assert!( + out.contains("movdqu XMMWORD PTR [rbp - 112], xmm6"), + "{out}" + ); + assert!( + out.contains("movdqu XMMWORD PTR [rbp - 256], xmm15"), + "{out}" + ); + assert!( + out.contains("movdqu xmm15, XMMWORD PTR [rbp - 256]"), + "{out}" + ); + assert!( + out.contains("movdqu xmm6, XMMWORD PTR [rbp - 112]"), + "{out}" + ); + assert!(out.contains("mov rsi, QWORD PTR [rbp - 88]"), "{out}"); + assert!(out.contains("mov rdi, QWORD PTR [rbp - 80]"), "{out}"); + } + + /// Verifies the SysV trampoline remains free of MSx64-only register saves. + #[test] + fn linux_extern_callback_omits_msx64_nonvolatile_register_saves() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + let trampoline = DeferredExternCallbackTrampoline { + label: "ffi_callback".to_string(), + descriptor_slot_label: "ffi_callback_descriptor".to_string(), + visible_arg_types: vec![PhpType::Int], + return_type: PhpType::Float, + }; + + emit_x86_64_extern_callback_trampoline(&mut emitter, &trampoline); + let out = emitter.output(); + + assert!(!out.contains("XMMWORD PTR"), "{out}"); + assert!(!out.contains("[rbp - 80], rdi"), "{out}"); + assert!(!out.contains("[rbp - 88], rsi"), "{out}"); + } +} diff --git a/src/codegen_support/wrappers/fiber.rs b/src/codegen_support/wrappers/fiber.rs index 891bbd4645..c87ee1f593 100644 --- a/src/codegen_support/wrappers/fiber.rs +++ b/src/codegen_support/wrappers/fiber.rs @@ -231,7 +231,7 @@ fn emit_x86_64_descriptor_invoker_wrapper(emitter: &mut Emitter, label: &str) { 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.emit_platform_callback_call("r10", 2); 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 @@ -626,7 +626,10 @@ fn emit_x86_64_wrapper(emitter: &mut Emitter, wrapper: &DeferredFiberWrapper) { spill_wrapper_args_x86_64(emitter, wrapper, &arg_types, "r14"); let overflow_bytes = materialize_spilled_args_for_closure_call_x86_64(emitter, &arg_types); + let call_stack_padding = abi::outgoing_call_stack_pad_bytes(emitter.target, overflow_bytes); + abi::emit_reserve_temporary_stack(emitter, call_stack_padding); abi::emit_call_reg(emitter, "r13"); + abi::emit_release_temporary_stack(emitter, call_stack_padding); 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()); @@ -858,3 +861,39 @@ fn frame_arg_slot_offset(idx: usize) -> usize { fn align16(n: usize) -> usize { (n + 15) & !15 } + +#[cfg(test)] +mod tests { + use crate::codegen_support::emit::Emitter; + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::*; + + /// Verifies the windows-x86_64 descriptor-invoker Fiber wrapper emits the + /// reverse-ABI SysV->MSx64 remap immediately before the indirect `call r10` + /// into the generated uniform invoker (finding F1, reverse-ABI): without it, + /// the invoker would read its descriptor/argument-array arguments from the + /// wrong registers (rdi/rsi instead of rcx/rdx) on windows-x86_64. + #[test] + fn test_windows_x86_64_descriptor_invoker_wrapper_remaps_before_indirect_call() { + let mut emitter = Emitter::new(Target::new(Platform::Windows, Arch::X86_64)); + emit_x86_64_descriptor_invoker_wrapper(&mut emitter, "fiber_invoker_0"); + let asm = emitter.output(); + + let remap_idx = asm.find("mov rcx, rdi").expect("expected SysV->MSx64 remap"); + let call_idx = asm.find("call r11").expect("expected relocated indirect call r11"); + assert!(remap_idx < call_idx, "remap must precede the indirect invoker call"); + } + + /// Verifies linux-x86_64 emission never sees the reverse-ABI remap: the remap + /// is windows-x86_64-only, so a linux-x86_64 descriptor-invoker wrapper must + /// stay byte-identical to before the remap was introduced. + #[test] + fn test_linux_x86_64_descriptor_invoker_wrapper_has_no_reverse_abi_remap() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_x86_64_descriptor_invoker_wrapper(&mut emitter, "fiber_invoker_0"); + let asm = emitter.output(); + + assert!(!asm.contains("mov rcx, rdi")); + } +} diff --git a/src/debug_info.rs b/src/debug_info.rs index cba4079dcb..e79584bf05 100644 --- a/src/debug_info.rs +++ b/src/debug_info.rs @@ -18,6 +18,8 @@ //! linker needs it to build the `N_SO` stab; without it the debug map is //! silently dropped). All of it is hand-encoded here, the same way //! `-g`-enabled assemblers do it. +//! - Windows GNU/COFF uses the standard DWARF section names with the `"dr"` +//! characteristics accepted by MinGW GAS (discardable, read-only debug data). //! - Markers only appear in the text section, which keeps `.loc` legal. use std::fmt::Write as _; @@ -100,7 +102,10 @@ fn debug_info_sections( ".section .debug_abbrev,\"\",@progbits", ".section .debug_info,\"\",@progbits", ), - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => ( + ".section .debug_abbrev,\"dr\"", + ".section .debug_info,\"dr\"", + ), }; let name = escape_asm_string(source_path); let comp_dir = escape_asm_string( @@ -229,6 +234,17 @@ _php_foo: linux.contains(".section .debug_info,\"\",@progbits"), "{linux}" ); + + let windows = inject_line_directives(ASM, "a.php", Platform::Windows); + assert!( + windows.contains(".section .debug_abbrev,\"dr\""), + "{windows}" + ); + assert!( + windows.contains(".section .debug_info,\"dr\""), + "{windows}" + ); + assert!(windows.contains(".loc 1 3 5"), "{windows}"); } /// Verifies non-marker lines pass through untouched and malformed markers diff --git a/src/ir/runtime_call.rs b/src/ir/runtime_call.rs index 264de8120a..5f97c92eec 100644 --- a/src/ir/runtime_call.rs +++ b/src/ir/runtime_call.rs @@ -78,6 +78,8 @@ pub enum UnaryStringRuntime { Base64Decode, Base64Encode, BinToHex, + EscapeShellArg, + EscapeShellCmd, HexToBin, HtmlEntityDecode, NlToBr, @@ -99,6 +101,8 @@ impl UnaryStringRuntime { UnaryStringRuntime::Base64Decode => "string.base64_decode", UnaryStringRuntime::Base64Encode => "string.base64_encode", UnaryStringRuntime::BinToHex => "string.bin_to_hex", + UnaryStringRuntime::EscapeShellArg => "string.escape_shell_arg", + UnaryStringRuntime::EscapeShellCmd => "string.escape_shell_cmd", UnaryStringRuntime::HexToBin => "string.hex_to_bin", UnaryStringRuntime::HtmlEntityDecode => "string.html_entity_decode", UnaryStringRuntime::NlToBr => "string.nl_to_br", diff --git a/src/ir/runtime_fn.rs b/src/ir/runtime_fn.rs index 37d3d06387..cdd13b0547 100644 --- a/src/ir/runtime_fn.rs +++ b/src/ir/runtime_fn.rs @@ -233,6 +233,10 @@ pub enum RuntimeFnId { Pclose, Pfsockopen, Popen, + ProcClose, + ProcGetStatus, + ProcOpen, + ProcTerminate, PrintR, Readdir, Readfile, @@ -326,6 +330,7 @@ pub enum RuntimeFnId { Pow, Rad2deg, Rand, + RandomBytes, RandomInt, Round, Sin, @@ -488,12 +493,21 @@ pub enum RuntimeFnId { impl RuntimeFnId { /// Returns the central logical ABI and backend contract for this runtime function. pub fn descriptor(self) -> RuntimeFnDescriptor { - let logical_signature = crate::builtins::registry::runtime_fn_arity_bounds(self).map( - |(min_operands, max_operands)| crate::ir::RuntimeCallSignature::Polymorphic { - min_operands, - max_operands, - }, - ); + let logical_signature = if self == RuntimeFnId::ProcOpen { + // `proc_open` appends command-line, environment-block, and flag operands + // after normalizing the six caller-visible PHP parameters. + Some(crate::ir::RuntimeCallSignature::Polymorphic { + min_operands: 9, + max_operands: Some(9), + }) + } else { + crate::builtins::registry::runtime_fn_arity_bounds(self).map( + |(min_operands, max_operands)| crate::ir::RuntimeCallSignature::Polymorphic { + min_operands, + max_operands, + }, + ) + }; RuntimeFnDescriptor { id: self, eir_name: self.as_eir(), @@ -768,7 +782,10 @@ impl RuntimeFnId { RuntimeFnId::HashHmac => &[BuiltinRequirement::Bridge("elephc_crypto")], RuntimeFnId::HashInit => &[BuiltinRequirement::Bridge("elephc_crypto")], RuntimeFnId::HashUpdate => &[BuiltinRequirement::Bridge("elephc_crypto")], - RuntimeFnId::MbStrlen => &[BuiltinRequirement::MacOsLibrary("iconv")], + RuntimeFnId::MbStrlen => &[ + BuiltinRequirement::MacOsLibrary("iconv"), + BuiltinRequirement::WindowsLibrary("iconv"), + ], RuntimeFnId::Md5 => &[BuiltinRequirement::Bridge("elephc_crypto")], RuntimeFnId::Sha1 => &[BuiltinRequirement::Bridge("elephc_crypto")], RuntimeFnId::StreamSocketEnableCrypto => &[BuiltinRequirement::Bridge("elephc_tls")], @@ -827,6 +844,21 @@ impl RuntimeFnId { matches!(self, RuntimeFnId::MbStrlen) } + /// Returns whether this operation needs Windows timezone-offset publication. + pub const fn uses_windows_timezone_bridge(self) -> bool { + matches!( + self, + RuntimeFnId::Date + | RuntimeFnId::Gmdate + | RuntimeFnId::Mktime + | RuntimeFnId::Gmmktime + | RuntimeFnId::Strtotime + | RuntimeFnId::ElephcMktimeRaw + | RuntimeFnId::ElephcGmmktimeRaw + | RuntimeFnId::ElephcStrtotimeRaw + ) + } + /// Returns whether this operation can publish PHAR bridge helper symbols. pub const fn publishes_phar_symbols(self) -> bool { matches!( @@ -1138,6 +1170,10 @@ impl RuntimeFnId { RuntimeFnId::Pclose => "pclose", RuntimeFnId::Pfsockopen => "pfsockopen", RuntimeFnId::Popen => "popen", + RuntimeFnId::ProcClose => "proc_close", + RuntimeFnId::ProcGetStatus => "proc_get_status", + RuntimeFnId::ProcOpen => "proc_open", + RuntimeFnId::ProcTerminate => "proc_terminate", RuntimeFnId::PrintR => "print_r", RuntimeFnId::Readdir => "readdir", RuntimeFnId::Readfile => "readfile", @@ -1231,6 +1267,7 @@ impl RuntimeFnId { RuntimeFnId::Pow => "pow", RuntimeFnId::Rad2deg => "rad2deg", RuntimeFnId::Rand => "rand", + RuntimeFnId::RandomBytes => "random_bytes", RuntimeFnId::RandomInt => "random_int", RuntimeFnId::Round => "round", RuntimeFnId::Sin => "sin", diff --git a/src/ir_lower/context.rs b/src/ir_lower/context.rs index 45dbe4884b..8c89ddf006 100644 --- a/src/ir_lower/context.rs +++ b/src/ir_lower/context.rs @@ -13,6 +13,7 @@ use std::collections::{BTreeSet, HashMap, HashSet}; +use crate::codegen_support::platform::Platform; use crate::ir::{ BlockId, Builder, DataId, DataPool, Effects, Function, Immediate, IrType, LocalKind, LocalSlotId, Op, Ownership, ValueId, @@ -98,6 +99,7 @@ const EVAL_ARGV_LOCAL_NAME: &str = "argv"; pub(crate) struct LoweringContext<'m, 'f> { pub builder: Builder<'f>, pub data: &'m mut DataPool, + pub platform: Platform, pub local_slots: HashMap, pub local_kinds: HashMap, pub local_types: TypeEnv, @@ -176,6 +178,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { pub(crate) fn new( builder: Builder<'f>, data: &'m mut DataPool, + platform: Platform, env: TypeEnv, functions: &'m HashMap, extern_functions: &'m HashMap, @@ -205,6 +208,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { Self { builder, data, + platform, local_slots: HashMap::new(), local_kinds: HashMap::new(), local_types: env, diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 0b9ad19f6a..21a31b9ae9 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -25,7 +25,6 @@ use crate::parser::ast::{ InstanceOfTarget, MagicConstant, StaticReceiver, Stmt, StmtKind, TypeExpr, Visibility, }; use crate::span::Span; -use crate::types::checker::builtins::canonical_builtin_function_name; use crate::types::{ checker::infer_expr_type_syntactic, merge_array_key_types, normalized_array_key_type, ExternFunctionSig, FunctionSig, PhpType, ReturnArgAlias, ThrowAccessKind, @@ -1969,7 +1968,11 @@ fn lower_function_call(ctx: &mut LoweringContext<'_, '_>, name: &Name, args: &[E } if ctx.has_eval_barrier() && plain_positional_call_args(args) - && canonical_builtin_function_name(canonical).is_none() + && crate::types::checker::builtins::canonical_builtin_function_name_on_platform( + canonical, + ctx.platform, + ) + .is_none() { let dynamic_name = php_symbol_key(canonical.trim_start_matches('\\')); let data = ctx.intern_function_name(&dynamic_name); @@ -1983,7 +1986,47 @@ fn lower_function_call(ctx: &mut LoweringContext<'_, '_>, name: &Name, args: &[E ); } let eval_literal = eval_literal_fragment(canonical, args); - emit_builtin_call_value(ctx, canonical, operands, php_type, expr.span, eval_literal) + let call = emit_builtin_call_value(ctx, canonical, operands, php_type, expr.span, eval_literal); + apply_builtin_output_type_flow(ctx, canonical, args); + call +} + +/// Applies post-call local type facts for builtins with documented by-reference outputs. +/// +/// Checker flow records the same facts while processing statement expressions. +/// Lowering must mirror them because an earlier source assignment (for example, +/// `$pipes = []`) replaces the final checker snapshot with its pre-call `never` +/// element type as the EIR local is built. Only `proc_open` is modeled here: its +/// plain-variable `$pipes` output is an integer-keyed map of stream resources. +/// +/// Its EIR storage element is deliberately `Mixed`, rather than the checker-facing +/// `resource` fact: `__rt_proc_open` creates resource boxes and publishes +/// those boxes through `__rt_array_set_mixed_key` (hash value tag 7). Keeping the +/// physical entry type as `Mixed` makes `HashGet` retain the box and lets stream +/// consumers unbox its `{ tag: resource, fd, kind }` payload. Representing the +/// same table as `Resource` would lower reads as raw integers because resources +/// use the scalar codegen representation, turning a Mixed pointer into an fd. +fn apply_builtin_output_type_flow( + ctx: &mut LoweringContext<'_, '_>, + name: &str, + args: &[Expr], +) { + if php_symbol_key(name.trim_start_matches('\\')) != "proc_open" { + return; + } + let Some(pipes) = crate::builtins::proc_open_argument_at(args, 2, "pipes") else { + return; + }; + let ExprKind::Variable(name) = &pipes.kind else { + return; + }; + ctx.set_local_type( + name, + PhpType::AssocArray { + key: Box::new(PhpType::Int), + value: Box::new(PhpType::Mixed), + }, + ); } /// Emits a builtin call and releases owned temporary arguments after the call consumes them. @@ -4559,7 +4602,12 @@ fn resolve_static_string_callable( if let Some(function_name) = lookup_folded_name(ctx.extern_functions.keys(), callback) { return Some(StaticCallableBinding::ExternFunction(function_name)); } - if let Some(function_name) = canonical_builtin_function_name(callback) { + if let Some(function_name) = + crate::types::checker::builtins::canonical_builtin_function_name_on_platform( + callback, + ctx.platform, + ) + { return Some(StaticCallableBinding::Builtin(function_name)); } if let Some(function_name) = lookup_folded_name(ctx.functions.keys(), callback) { @@ -5232,6 +5280,9 @@ fn lower_builtin_call_args( crate::builtins::semantics::BuiltinArgumentLowering::JsonDecode => { lower_json_decode_args(ctx, sig, args) } + crate::builtins::semantics::BuiltinArgumentLowering::ProcOpen => { + lower_proc_open_args(ctx, sig, args) + } crate::builtins::semantics::BuiltinArgumentLowering::PregReplaceCallback if !crate::types::call_args::has_named_args(args) && !args.iter().any(is_spread_arg) => @@ -5284,6 +5335,98 @@ fn lower_positional_builtin_args_with_signature( .collect() } +/// Lowers the Windows-only static `proc_open` extensions into a compact runtime +/// ABI: command arrays become a correctly quoted direct command line, environment +/// maps become a counted double-NUL block, and the final integer packs the block +/// byte length plus the direct-execution bit. +fn lower_proc_open_args( + ctx: &mut LoweringContext<'_, '_>, + sig: Option<&FunctionSig>, + args: &[Expr], +) -> Vec { + if args.iter().any(is_spread_arg) { + return lower_args_with_signature(ctx, sig, args); + } + if crate::types::call_args::has_named_args(args) { + let mut operands = lower_args_with_signature(ctx, sig, args); + let command = crate::builtins::proc_open_argument_at(args, 0, "command") + .expect("checked proc_open call has command"); + let command_line = crate::builtins::static_windows_command_line(command); + let environment_expr = crate::builtins::proc_open_argument_at(args, 4, "env_vars"); + let environment = environment_expr + .and_then(crate::builtins::static_windows_environment_block); + let option_flags = crate::builtins::proc_open_argument_at(args, 5, "options") + .and_then(crate::builtins::static_windows_options) + .unwrap_or(0); + if let Some(command_line) = command_line.as_ref() { + let synthetic = Expr::new(ExprKind::StringLiteral(command_line.clone()), command.span); + operands.push(lower_expr(ctx, &synthetic).value); + } else { + let null = Expr::new(ExprKind::Null, command.span); + operands.push(lower_expr(ctx, &null).value); + } + if let Some(environment) = environment.as_ref() { + let span = environment_expr.map_or(command.span, |expr| expr.span); + let synthetic = Expr::new(ExprKind::StringLiteral(environment.clone()), span); + operands.push(lower_expr(ctx, &synthetic).value); + } else { + let null = Expr::new(ExprKind::Null, command.span); + operands.push(lower_expr(ctx, &null).value); + } + let environment_len = environment.as_ref().map_or(0, String::len) as i64; + let flags = (environment_len << crate::builtins::WINDOWS_PROC_OPTION_BITS) + | option_flags + | i64::from(command_line.is_some()); + let flags_expr = Expr::new(ExprKind::IntLiteral(flags), command.span); + operands.push(lower_expr(ctx, &flags_expr).value); + return operands; + } + let command_line = args + .first() + .and_then(crate::builtins::static_windows_command_line); + let environment = args + .get(4) + .and_then(crate::builtins::static_windows_environment_block); + let option_flags = args + .get(5) + .and_then(crate::builtins::static_windows_options) + .unwrap_or(0); + let direct = command_line.is_some(); + + let mut operands = Vec::with_capacity(9); + for index in 0..args.len().min(6) { + operands.push(match sig { + Some(sig) => lower_arg_with_signature(ctx, sig, index, &args[index]), + None => lower_expr(ctx, &args[index]).value, + }); + } + while operands.len() < 6 { + let default = Expr::new(ExprKind::Null, args[0].span); + operands.push(lower_expr(ctx, &default).value); + } + if let Some(command_line) = command_line { + let synthetic = Expr::new(ExprKind::StringLiteral(command_line), args[0].span); + operands.push(lower_expr(ctx, &synthetic).value); + } else { + let null = Expr::new(ExprKind::Null, args[0].span); + operands.push(lower_expr(ctx, &null).value); + } + if let Some(environment) = environment.as_ref() { + let synthetic = Expr::new(ExprKind::StringLiteral(environment.clone()), args[4].span); + operands.push(lower_expr(ctx, &synthetic).value); + } else { + let null = Expr::new(ExprKind::Null, args[0].span); + operands.push(lower_expr(ctx, &null).value); + } + let environment_len = environment.as_ref().map_or(0, String::len) as i64; + let flags = (environment_len << crate::builtins::WINDOWS_PROC_OPTION_BITS) + | option_flags + | i64::from(direct); + let flags_expr = Expr::new(ExprKind::IntLiteral(flags), args[0].span); + operands.push(lower_expr(ctx, &flags_expr).value); + operands +} + /// Lowers `count()` arguments, dropping a statically-default mode argument. /// /// The EIR backend implements only `COUNT_NORMAL`; a literal `0` mode (named @@ -5718,7 +5861,7 @@ fn coerce_scalar_arg_to_param_storage( return coerce_to_float(ctx, value, arg); } let source_ty = ctx.builder.value_php_type(value.value).codegen_repr(); - if param_ty == PhpType::Str && matches!(source_ty, PhpType::Mixed | PhpType::Union(_)) { + if param_ty == PhpType::Str && is_php_string_coercible_param_type(&source_ty) { return coerce_to_string(ctx, value, arg); } value @@ -5753,9 +5896,7 @@ fn coerce_operands_to_params( ir_type: IrType::I64, }; operands[index] = coerce_to_float_at_span(ctx, lowered, None).value; - } else if param_ty == PhpType::Str - && matches!(operand_ty, PhpType::Mixed | PhpType::Union(_)) - { + } else if param_ty == PhpType::Str && is_php_string_coercible_param_type(&operand_ty) { let lowered = LoweredValue { value, ir_type: ctx.builder.value_type(value), @@ -5766,6 +5907,25 @@ fn coerce_operands_to_params( operands } +/// Returns whether a declared PHP string parameter accepts this lowered scalar representation. +/// +/// Arrays and objects deliberately remain excluded: PHP weak scalar coercion accepts numbers, +/// booleans, null, and dynamic scalar unions, but not compound/object arguments for typed string +/// internal functions such as `escapeshellarg()`. +fn is_php_string_coercible_param_type(ty: &PhpType) -> bool { + matches!( + ty, + PhpType::Int + | PhpType::Float + | PhpType::Bool + | PhpType::Void + | PhpType::Never + | PhpType::TaggedScalar + | PhpType::Mixed + | PhpType::Union(_) + ) +} + /// Widens local indexed-array storage before passing it to an `array` ref parameter. fn lower_by_ref_array_arg_with_signature( ctx: &mut LoweringContext<'_, '_>, @@ -12055,12 +12215,19 @@ fn resolve_known_reflection_function_name( function_name: &str, ) -> Option { resolve_known_function_name(ctx, function_name) - .or_else(|| resolve_known_reflection_builtin_name(function_name)) + .or_else(|| resolve_known_reflection_builtin_name(ctx, function_name)) } /// Resolves a supported callable builtin name for `ReflectionFunction`. -fn resolve_known_reflection_builtin_name(function_name: &str) -> Option { - let canonical = canonical_builtin_function_name(function_name.trim_start_matches('\\'))?; +fn resolve_known_reflection_builtin_name( + ctx: &LoweringContext<'_, '_>, + function_name: &str, +) -> Option { + let canonical = + crate::types::checker::builtins::canonical_builtin_function_name_on_platform( + function_name.trim_start_matches('\\'), + ctx.platform, + )?; first_class_builtin_signature(&canonical).map(|_| canonical) } @@ -14557,6 +14724,16 @@ fn coerce_to_string_at_span( span, ); } + if ctx.builder.value_php_type(value.value).codegen_repr() == PhpType::Bool { + return ctx.emit_value( + Op::BoolToStr, + vec![value.value], + None, + PhpType::Str, + Op::BoolToStr.default_effects(), + span, + ); + } match value.ir_type { IrType::Str => value, IrType::I64 | IrType::TaggedScalar => ctx.emit_value(Op::IToStr, vec![value.value], None, PhpType::Str, Op::IToStr.default_effects(), span), diff --git a/src/ir_lower/function.rs b/src/ir_lower/function.rs index 143518f329..2e32222ffc 100644 --- a/src/ir_lower/function.rs +++ b/src/ir_lower/function.rs @@ -64,6 +64,7 @@ pub(crate) fn lower_main( let closures = lower_body_into_function( &mut function, &mut module.data, + module.target.platform, program, top_level_env.clone(), top_level_env, @@ -258,6 +259,7 @@ pub(crate) fn lower_user_function( let closures = lower_body_into_function( &mut function, &mut module.data, + module.target.platform, body, env_from_signature(&eir_signature, web), web_gated_global_env(&check_result.global_env, web), @@ -358,6 +360,7 @@ pub(crate) fn lower_class_method( let closures = lower_body_into_function( &mut function, &mut module.data, + module.target.platform, body, env, web_gated_global_env(&check_result.global_env, web), @@ -423,6 +426,7 @@ pub(crate) fn lower_eval_aot_function( let closures = lower_body_into_function( &mut function, &mut module.data, + module.target.platform, body, TypeEnv::new(), check_result.global_env.clone(), @@ -528,6 +532,7 @@ pub(crate) fn lower_eval_aot_scope_function( let closures = lower_body_into_function( &mut function, &mut module.data, + module.target.platform, body, env, check_result.global_env.clone(), @@ -627,6 +632,7 @@ pub(crate) fn lower_property_init_thunk( let closures = lower_body_into_function( &mut function, &mut module.data, + module.target.platform, &body, env, web_gated_global_env(&check_result.global_env, web), @@ -822,6 +828,7 @@ fn lower_closure_function_with_signature( let closures = lower_body_into_function( &mut function, parent.data, + parent.platform, body, env, parent.top_level_env.clone(), @@ -858,6 +865,7 @@ fn lower_closure_function_with_signature( fn lower_body_into_function( function: &mut Function, data: &mut crate::ir::DataPool, + platform: crate::codegen_support::platform::Platform, body: &[Stmt], env: TypeEnv, top_level_env: TypeEnv, @@ -905,6 +913,7 @@ fn lower_body_into_function( let mut ctx = LoweringContext::new( builder, data, + platform, env, functions, extern_functions, diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index d1925a1a88..193d6f4572 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -90,11 +90,7 @@ pub(crate) fn lower( /// Converts a PHP source path into the canonical display string stored in EIR metadata. fn canonical_source_path(source_path: &Path) -> String { - source_path - .canonicalize() - .unwrap_or_else(|_| source_path.to_path_buf()) - .display() - .to_string() + crate::source_path::canonical_source_path(source_path) } /// Copies declaration metadata into the EIR module placeholder tables. @@ -148,6 +144,24 @@ fn populate_metadata(module: &mut Module, program: &Program, check_result: &Chec .collect(); module.required_runtime_features = crate::codegen::runtime_features_for_program_and_classes(program, &check_result.classes); + include_required_library_runtime_features( + &mut module.required_runtime_features, + &check_result.required_libraries, + ); +} + +/// Enables optional runtime adapter families implied by native libraries selected +/// during type checking, keeping EIR-driven runtime generation and final linking +/// on the same feature set. +fn include_required_library_runtime_features( + features: &mut RuntimeFeatures, + required_libraries: &[String], +) { + let requires = |name: &str| required_libraries.iter().any(|library| library == name); + features.zlib |= requires("z"); + features.bzip2 |= requires("bz2"); + features.iconv |= requires("iconv"); + features.tls |= requires("elephc_tls"); } /// Normalizes class method metadata to the ABI contracts emitted in EIR. @@ -374,6 +388,7 @@ pub(super) fn include_lowered_runtime_features(module: &mut Module) { module.required_runtime_features.descriptor_invoker |= features.descriptor_invoker; module.required_runtime_features.eval_bridge |= features.eval_bridge; module.required_runtime_features.eval_scope |= features.eval_scope; + module.required_runtime_features.timezone_bridge |= features.timezone_bridge; } /// Derives optional runtime features from the actual EIR instruction stream. @@ -396,6 +411,9 @@ fn lowered_runtime_features(module: &Module) -> RuntimeFeatures { && function_belongs_to_phar_archive_helper_class(function); features.descriptor_invoker |= typed_builtin_requires_descriptor_invoker(function, inst, target); + features.timezone_bridge |= module.target.platform + == crate::codegen::platform::Platform::Windows + && target.uses_windows_timezone_bridge(); } } Op::LanguageConstructCall => { diff --git a/src/ir_lower/tests/mod.rs b/src/ir_lower/tests/mod.rs index 6b011ad60b..8dbcc0066e 100644 --- a/src/ir_lower/tests/mod.rs +++ b/src/ir_lower/tests/mod.rs @@ -21,7 +21,12 @@ mod ownership; /// Runs frontend, type checking, optimization, and EIR lowering for a source string. fn lower_source(source: &str) -> crate::ir::Module { - lower_source_at(source, Path::new("main.php"), Path::new(".")) + lower_source_with_target(source, Target::detect_host()) +} + +/// Runs frontend and EIR lowering for a source string using an explicit target. +fn lower_source_with_target(source: &str, target: Target) -> crate::ir::Module { + lower_source_at_target(source, Path::new("main.php"), Path::new("."), target) } /// Runs frontend, type checking, optimization, and EIR lowering for a file. @@ -33,7 +38,16 @@ fn lower_file(path: &Path) -> crate::ir::Module { /// Runs the `--emit-ir` frontend ordering for a source string and base path. fn lower_source_at(source: &str, main_file_path: &Path, parent: &Path) -> crate::ir::Module { - let target = Target::detect_host(); + lower_source_at_target(source, main_file_path, parent, Target::detect_host()) +} + +/// Runs the `--emit-ir` frontend ordering with an explicit target selection. +fn lower_source_at_target( + source: &str, + main_file_path: &Path, + parent: &Path, + target: Target, +) -> crate::ir::Module { let tokens = crate::lexer::tokenize(source).expect("tokenize failed"); let parsed = crate::parser::parse(&tokens).expect("parse failed"); let main_file_path = PathBuf::from(main_file_path); @@ -47,8 +61,11 @@ fn lower_source_at(source: &str, main_file_path: &Path, parent: &Path) -> crate: let ast = crate::list_id_prelude::inject_if_used(ast); let ast = crate::var_export_prelude::inject_if_used(ast); let ast = crate::image_prelude::inject_if_used(ast, false); - let ast = crate::name_resolver::resolve(ast).expect("name resolution failed"); - let ast = crate::autoload::run(ast, parent, &autoload_registry).expect("autoload failed"); + let ast = crate::name_resolver::resolve_for_platform(ast, target.platform) + .expect("name resolution failed"); + let ast = + crate::autoload::run_for_platform(ast, parent, &autoload_registry, target.platform) + .expect("autoload failed"); let ast = crate::optimize::fold_constants(ast); let check_result = crate::types::check_with_target(&ast, target).expect("type check failed"); let ast = crate::optimize::propagate_constants(ast); @@ -63,6 +80,62 @@ fn lower_source_at(source: &str, main_file_path: &Path, parent: &Path) -> crate: }) } +/// Verifies computed Windows proc_open settings remain real EIR operands while +/// the three hidden marshalling operands preserve the nine-operand ABI shape. +#[test] +fn lowers_dynamic_windows_proc_open_marshalling_operands() { + let module = lower_source_with_target( + r#" 42]; +$options = ['bypass_shell' => true]; +proc_open($command, [1 => ['pipe', 'w']], $pipes, null, $environment, $options); +"#, + Target::new( + crate::codegen::platform::Platform::Windows, + crate::codegen::platform::Arch::X86_64, + ), + ); + let call = module + .functions + .iter() + .flat_map(|function| &function.instructions) + .find(|instruction| { + instruction.op == crate::ir::Op::RuntimeCall + && instruction.immediate + == Some(crate::ir::Immediate::RuntimeCall( + crate::ir::RuntimeCallTarget::Function(crate::ir::RuntimeFnId::ProcOpen), + )) + }) + .unwrap_or_else(|| panic!("missing proc_open EIR call: {}", print_module(&module))); + assert_eq!(call.operands.len(), 9, "proc_open hidden marshalling ABI changed"); +} + +/// Verifies proc_open pipe reads retain the boxed Mixed resource published by +/// the keyed runtime setter instead of treating its pointer as a raw fd. +#[test] +fn lowers_proc_open_pipe_readback_as_mixed_storage() { + let module = lower_source( + r#" ["pipe", "w"]], $pipes); +echo fread($pipes[1], 100); +"#, + ); + let read = module + .functions + .iter() + .flat_map(|function| &function.instructions) + .find(|instruction| instruction.op == crate::ir::Op::HashGet) + .unwrap_or_else(|| panic!("missing proc_open pipe lookup: {}", print_module(&module))); + assert_eq!( + read.result_php_type.codegen_repr(), + crate::types::PhpType::Mixed, + "proc_open publishes boxed resource entries through the Mixed hash contract", + ); +} + /// Verifies lowering emits valid EIR for functions, arrays, foreach, and loops. #[test] fn lowers_control_flow_arrays_and_functions() { diff --git a/src/ir_passes/regalloc.rs b/src/ir_passes/regalloc.rs index d8458c143e..e2593444e3 100644 --- a/src/ir_passes/regalloc.rs +++ b/src/ir_passes/regalloc.rs @@ -23,7 +23,7 @@ use std::collections::{HashMap, HashSet}; -use crate::codegen::platform::{Arch, Target}; +use crate::codegen::platform::{Arch, Platform, Target}; use crate::ir::{Function, IrType, Op, Ownership, Terminator, ValueId}; use crate::ir_passes::allocation::Allocation; use crate::ir_passes::intervals::{build_intervals, LiveInterval}; @@ -125,11 +125,11 @@ fn is_eligible(func: &Function, iv: &LiveInterval, ineligible: &HashSet /// function must save/restore the register (only callee-saved pools). #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum PoolKind { - /// Caller-saved integer pool (`x12`–`x15` / `rsi`,`rdi`,`r8`,`r9`). + /// Caller-saved integer pool (`x12`–`x15`; target-valid x86_64 volatile registers). CallerInt, /// Callee-saved integer pool (`x21`–`x28` / `rbx`). CalleeInt, - /// Caller-saved float pool (`d16`–`d23` / `xmm2`–`xmm7`). + /// Caller-saved float pool (`d16`–`d23`; target-valid x86_64 volatile XMM registers). CallerFloat, /// Callee-saved float pool (`d8`–`d14`; empty on x86_64). CalleeFloat, @@ -349,12 +349,16 @@ fn callee_float_pool(target: Target) -> &'static [&'static str] { /// op lowerings (see `crate::ir_passes::clobber`) never touch them, so a /// call-free value keeps its register intact with no save/restore. fn caller_int_pool(target: Target) -> &'static [&'static str] { - match target.arch { + match (target.platform, target.arch) { // x12–x15 are caller-saved temporaries the volatile-safe lowerings never use. - Arch::AArch64 => &["x12", "x13", "x14", "x15"], + (_, Arch::AArch64) => &["x12", "x13", "x14", "x15"], // rsi/rdi/r8/r9 are caller-saved argument registers, untouched by the // volatile-safe lowerings (which use rax/rdx/rcx/r10/r11 only). - Arch::X86_64 => &["rsi", "rdi", "r8", "r9"], + (Platform::MacOS | Platform::Linux, Arch::X86_64) => &["rsi", "rdi", "r8", "r9"], + // MSx64 makes rsi/rdi nonvolatile. Generated Windows frames do not save + // them, so only the volatile r8/r9 subset is eligible as call-free + // allocation storage. + (Platform::Windows, Arch::X86_64) => &["r8", "r9"], } } @@ -364,10 +368,16 @@ fn caller_int_pool(target: Target) -> &'static [&'static str] { /// way float values are register-allocated at all, enabled for call-free /// intervals. The volatile-safe float lowerings use only d0/d1 and xmm0/xmm1. fn caller_float_pool(target: Target) -> &'static [&'static str] { - match target.arch { + match (target.platform, target.arch) { // d16–d23 are caller-saved vector registers never used by the backend // as arguments, results, or scratch. - Arch::AArch64 => &["d16", "d17", "d18", "d19", "d20", "d21", "d22", "d23"], - Arch::X86_64 => &["xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7"], + (_, Arch::AArch64) => &["d16", "d17", "d18", "d19", "d20", "d21", "d22", "d23"], + (Platform::MacOS | Platform::Linux, Arch::X86_64) => { + &["xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7"] + } + // xmm6-xmm15 are nonvolatile under MSx64 and would require full 128-bit + // save/restore plus matching unwind codes. Keep allocation to the + // volatile xmm2-xmm5 subset until that frame support exists. + (Platform::Windows, Arch::X86_64) => &["xmm2", "xmm3", "xmm4", "xmm5"], } } diff --git a/src/ir_passes/tests/regalloc_test.rs b/src/ir_passes/tests/regalloc_test.rs index cfec1237f2..7aef953eed 100644 --- a/src/ir_passes/tests/regalloc_test.rs +++ b/src/ir_passes/tests/regalloc_test.rs @@ -175,6 +175,11 @@ fn x86_64() -> Target { Target::new(Platform::Linux, Arch::X86_64) } +/// The Windows x86_64 target used to verify MSx64 volatility constraints. +fn windows_x86_64() -> Target { + Target::new(Platform::Windows, Arch::X86_64) +} + /// Emits a void-result call that no other value consumes, acting purely as a /// clobber point between its surrounding instructions. fn emit_clobber_call(builder: &mut Builder<'_>) { @@ -219,6 +224,68 @@ fn value_live_across_call_uses_callee_saved() { ); } +/// Verifies an integer live across a Windows call uses the explicitly saved +/// rbx pool rather than MSx64-nonvolatile rsi/rdi as unsaved scratch storage. +#[test] +fn windows_value_live_across_call_uses_saved_rbx() { + let mut function = Function::new("win_across".to_string(), IrType::I64, PhpType::Int); + let live = { + let mut builder = Builder::new(&mut function); + let entry = builder.create_named_block("entry", vec![]); + builder.set_entry(entry); + builder.position_at_end(entry); + let value = builder.emit_const_i64(7); + emit_clobber_call(&mut builder); + let result = builder.emit_iadd(value, value); + builder.terminate(Terminator::Return { + value: Some(result), + }); + value + }; + + let allocation = allocate_registers(&function, windows_x86_64()); + assert_eq!(allocation.register_of(live), Some("rbx")); + assert_eq!(allocation.used_callee_saved(), &["rbx"]); +} + +/// Verifies call-free Windows integer allocations never use rsi/rdi, which are +/// nonvolatile under MSx64 and are not saved by generated frames. +#[test] +fn windows_call_free_integer_pool_excludes_rsi_and_rdi() { + let mut function = Function::new("win_ints".to_string(), IrType::I64, PhpType::Int); + let values = { + let mut builder = Builder::new(&mut function); + let entry = builder.create_named_block("entry", vec![]); + builder.set_entry(entry); + builder.position_at_end(entry); + let values = (0..6) + .map(|value| builder.emit_const_i64(value)) + .collect::>(); + let result = values + .iter() + .copied() + .reduce(|left, right| builder.emit_iadd(left, right)) + .expect("integer fixture has values"); + builder.terminate(Terminator::Return { + value: Some(result), + }); + values + }; + + let allocation = allocate_registers(&function, windows_x86_64()); + for value in values { + if let Some(register) = allocation.register_of(value) { + assert!( + matches!(register, "r8" | "r9" | "rbx"), + "Windows call-free allocation used unsupported register {register}" + ); + if register == "rbx" { + assert!(allocation.used_callee_saved().contains(&"rbx")); + } + } + } +} + /// Regression: a call's *result* must never use a caller-saved register, even /// when nothing else clobbers between the call and the result's use. The call /// lowering runs its argument-cleanup calls (`decref`) AFTER storing the result, @@ -300,6 +367,52 @@ fn call_free_float_uses_caller_saved_xmm_on_x86_64() { ); } +/// Verifies Windows call-free float allocation excludes xmm6-xmm15, whose full +/// 128-bit nonvolatile state generated frames do not currently preserve. +#[test] +fn windows_call_free_float_pool_stays_below_xmm6() { + let mut function = Function::new("win_floats".to_string(), IrType::F64, PhpType::Float); + let values = { + let mut builder = Builder::new(&mut function); + let entry = builder.create_named_block("entry", vec![]); + builder.set_entry(entry); + builder.position_at_end(entry); + let values = (0..8) + .map(|value| emit_const_f64(&mut builder, value as f64 + 0.5)) + .collect::>(); + let result = values + .iter() + .copied() + .reduce(|left, right| { + builder + .emit( + Op::FAdd, + vec![left, right], + None, + IrType::F64, + PhpType::Float, + Ownership::NonHeap, + ) + .expect("fadd produces a value") + }) + .expect("float fixture has values"); + builder.terminate(Terminator::Return { + value: Some(result), + }); + values + }; + + let allocation = allocate_registers(&function, windows_x86_64()); + for value in values { + if let Some(register) = allocation.register_of(value) { + assert!( + matches!(register, "xmm2" | "xmm3" | "xmm4" | "xmm5"), + "Windows float allocation used nonvolatile {register}" + ); + } + } +} + /// A float value that lives across a call on x86_64 cannot be register-allocated /// (no callee-saved XMM and the caller-saved pool is clobbered by the call), so /// it stays spilled. diff --git a/src/lexer/literals/identifiers.rs b/src/lexer/literals/identifiers.rs index 0d522a6006..3531bfc636 100644 --- a/src/lexer/literals/identifiers.rs +++ b/src/lexer/literals/identifiers.rs @@ -125,6 +125,7 @@ pub(in crate::lexer) fn scan_keyword(cursor: &mut Cursor) -> Result return Ok(Token::PhpEol), "PHP_OS" => return Ok(Token::PhpOs), "DIRECTORY_SEPARATOR" => return Ok(Token::DirectorySeparator), + "PATH_SEPARATOR" => return Ok(Token::PathSeparator), _ => {} } diff --git a/src/lexer/token.rs b/src/lexer/token.rs index c9a831de1d..17fa60f14f 100644 --- a/src/lexer/token.rs +++ b/src/lexer/token.rs @@ -145,6 +145,7 @@ pub enum Token { PhpEol, PhpOs, DirectorySeparator, + PathSeparator, DunderDir, DunderFile, DunderLine, @@ -349,6 +350,7 @@ impl Token { Token::PhpEol => Some("PHP_EOL"), Token::PhpOs => Some("PHP_OS"), Token::DirectorySeparator => Some("DIRECTORY_SEPARATOR"), + Token::PathSeparator => Some("PATH_SEPARATOR"), Token::DunderDir => Some("__DIR__"), Token::DunderFile => Some("__FILE__"), Token::DunderLine => Some("__LINE__"), diff --git a/src/lib.rs b/src/lib.rs index 154b36d57e..cd28847257 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,6 +54,7 @@ pub mod parser; pub mod pdo_prelude; /// Resolution of includes. pub mod resolver; +mod source_path; /// Source span tracking. pub mod span; /// `--strict-php` mode state and PHP-compatibility audit pass. diff --git a/src/linker.rs b/src/linker.rs index 7a2f85ce55..02a8a78ad2 100644 --- a/src/linker.rs +++ b/src/linker.rs @@ -127,8 +127,9 @@ const BRIDGES: &[BridgeStaticlib] = &[ env_var: "ELEPHC_WEB_LIB_DIR", crate_name: "elephc-web", flag_name: "web", - // The bridge owns the program entry (elephc_web_run) and tokio/hyper - // link-time machinery, so the whole archive is force-loaded. + // The bridge owns the program entry (elephc_web_run) and Tokio/Hyper + // link-time machinery. Windows is the sole exception: its GNU/COFF + // archive must stay a plain link to avoid duplicate import members. whole_archive: true, macos_frameworks: &[], // Rust runtime/unwinder symbols, like the other bridges. @@ -174,6 +175,18 @@ pub(crate) fn bridges_in(extra_link_libs: &[String]) -> Vec<(&'static str, &'sta } impl BridgeStaticlib { + /// Returns whether this bridge must be force-loaded on `platform`. + /// + /// `elephc_web` owns the native web entry point on Linux and macOS, but its + /// GNU/COFF archive must remain a plain link on Windows to avoid duplicate + /// import-library members. An explicitly forced archive still wins for all + /// other bridges. + fn requires_whole_archive(&self, platform: Platform, forced: bool) -> bool { + forced + || (self.whole_archive + && !(platform == Platform::Windows && self.lib_name == "elephc_web")) + } + /// Returns the `lib.a` archive filename this bridge produces. fn archive_filename(&self) -> String { format!("lib{}.a", self.lib_name) @@ -187,41 +200,81 @@ impl BridgeStaticlib { /// fallbacks. In a source checkout, builds the staticlib once when it is /// missing so `cargo run --` can compile examples without a manual /// `cargo build -p `. Returns `None` when it cannot be found or built. - fn lib_dir(&self) -> Option { + fn lib_dir(&self, target: Target) -> Option { if let Ok(env_dir) = std::env::var(self.env_var) { if !env_dir.is_empty() { return Some(env_dir); } } - if let Some(dir) = self.find_lib_dir() { + if let Some(dir) = self.find_lib_dir(target) { return Some(dir); } let workspace = self.find_workspace()?; - self.build_staticlib(&workspace); - self.find_lib_dir() + self.build_staticlib(&workspace, target); + self.find_lib_dir(target) } /// Returns the first candidate directory that currently contains the staticlib. - /// Order: the running binary's dir, its sibling `lib/`, `CARGO_TARGET_DIR` - /// profiles, then in-tree `target/{debug,release}`. - fn find_lib_dir(&self) -> Option { + /// For a native target, order is the running binary's dir, its sibling + /// `lib/`, `CARGO_TARGET_DIR` profiles, then in-tree profiles. For a cross + /// target, only Cargo's target-triple subdirectories are considered: a host + /// archive with the same filename must never shadow the PE/COFF archive. + fn find_lib_dir(&self, target: Target) -> Option { let archive = self.archive_filename(); let exe = std::env::current_exe().ok()?; let dir = exe.parent()?; - let mut candidates = vec![ - dir.to_path_buf(), - dir.parent().map(|parent| parent.join("lib")).unwrap_or_default(), - ]; + let native = target_is_native(target); + let mut candidates = Vec::new(); + let mut executable_cross_candidate = None; + if native { + candidates.push(dir.to_path_buf()); + candidates.push(dir.parent().map(|parent| parent.join("lib")).unwrap_or_default()); + } else { + // Derive Cargo's target root from either `target//elephc` + // or `target//deps/`. PE compilation tests + // execute the compiler from a temporary cwd, so cwd-relative + // `target/...` alone cannot discover the already-built COFF bridge. + let profile_dir = if dir.file_name().is_some_and(|name| name == "deps") { + dir.parent() + } else { + Some(dir) + }; + if let Some(profile_dir) = profile_dir { + if let (Some(target_root), Some(profile)) = + (profile_dir.parent(), profile_dir.file_name()) + { + executable_cross_candidate = Some( + target_root + .join(cargo_target_triple(target)) + .join(profile), + ); + } + } + } if let Ok(target_dir) = std::env::var("CARGO_TARGET_DIR") { if !target_dir.is_empty() { - candidates.push(PathBuf::from(&target_dir).join("debug")); - candidates.push(PathBuf::from(target_dir).join("release")); + let base = PathBuf::from(target_dir); + let base = if native { + base + } else { + base.join(cargo_target_triple(target)) + }; + candidates.push(base.join("debug")); + candidates.push(base.join("release")); } } + if let Some(candidate) = executable_cross_candidate { + candidates.push(candidate); + } // Fallbacks for source-tree builds where the process cwd is the // workspace root or a path below it. - candidates.push(PathBuf::from("target/debug")); - candidates.push(PathBuf::from("target/release")); + let source_target = if native { + PathBuf::from("target") + } else { + PathBuf::from("target").join(cargo_target_triple(target)) + }; + candidates.push(source_target.join("debug")); + candidates.push(source_target.join("release")); candidates .into_iter() @@ -242,13 +295,16 @@ impl BridgeStaticlib { /// Builds this bridge's staticlib in the current binary's debug/release /// profile (best-effort; failures are ignored so callers fall back to other /// discovery candidates). - fn build_staticlib(&self, workspace: &Path) { + fn build_staticlib(&self, workspace: &Path, target: Target) { let release = std::env::current_exe() .ok() .and_then(|exe| exe.parent().map(Path::to_path_buf)) .is_some_and(|dir| dir.file_name().is_some_and(|name| name == "release")); let mut cmd = Command::new("cargo"); cmd.args(["build", "-p", self.crate_name]); + if !target_is_native(target) { + cmd.args(["--target", cargo_target_triple(target)]); + } if release { cmd.arg("--release"); } @@ -256,29 +312,67 @@ impl BridgeStaticlib { } } +/// Returns whether `target` matches the compiler process OS, architecture, and +/// object ABI used by bridge archives. +/// +/// Elephc's Windows x86_64 output always uses the GNU/MinGW ABI. A compiler +/// built with Rust's native MSVC host toolchain must therefore discover and +/// build bridges in Cargo's `x86_64-pc-windows-gnu` target directory instead +/// of treating incompatible host `.lib` archives as native output. +fn target_is_native(target: Target) -> bool { + let platform_and_arch_match = target.platform == Platform::detect_host() + && match target.arch { + crate::codegen::platform::Arch::AArch64 => cfg!(target_arch = "aarch64"), + crate::codegen::platform::Arch::X86_64 => cfg!(target_arch = "x86_64"), + }; + let abi_matches = match (target.platform, target.arch) { + (Platform::Windows, crate::codegen::platform::Arch::X86_64) => { + cfg!(all(windows, target_env = "gnu")) + } + (Platform::Windows, crate::codegen::platform::Arch::AArch64) => { + cfg!(all(windows, target_env = "msvc")) + } + _ => true, + }; + platform_and_arch_match && abi_matches +} + +/// Returns Cargo's canonical Rust target triple for a bridge staticlib build. +fn cargo_target_triple(target: Target) -> &'static str { + use crate::codegen::platform::Arch; + match (target.platform, target.arch) { + (Platform::MacOS, Arch::AArch64) => "aarch64-apple-darwin", + (Platform::MacOS, Arch::X86_64) => "x86_64-apple-darwin", + (Platform::Linux, Arch::AArch64) => "aarch64-unknown-linux-gnu", + (Platform::Linux, Arch::X86_64) => "x86_64-unknown-linux-gnu", + (Platform::Windows, Arch::X86_64) => "x86_64-pc-windows-gnu", + (Platform::Windows, Arch::AArch64) => "aarch64-pc-windows-msvc", + } +} + /// Invokes the target assembler to produce an object file from assembly source. /// - `target`: Compiler target (controls assembler command and flags). /// - `asm_path`: Path to the generated `.s` assembly file. /// - `obj_path`: Output path for the resulting `.o` object file. /// Exits with status 1 if the assembler fails. pub(crate) fn assemble(target: Target, asm_path: &Path, obj_path: &Path) { - let mut as_cmd = Command::new(target.assembler_cmd()); - if target.platform == Platform::MacOS { - as_cmd.args(["-arch", target.darwin_arch_name()]); - } - as_cmd.arg("-o").arg(obj_path).arg(asm_path); + let mut as_cmd = if target.platform == Platform::Windows { + crate::windows_toolchain::assembler_command(asm_path, obj_path) + .unwrap_or_else(|message| fail_tool_configuration("Assembler", &message)) + } else { + let mut command = Command::new(target.assembler_cmd()); + if target.platform == Platform::MacOS { + command.args(["-arch", target.darwin_arch_name()]); + } + command.arg("-o").arg(obj_path).arg(asm_path); + command + }; run_tool("Assembler", &mut as_cmd); } -/// Makes `--debug-info` line tables reachable by debuggers after the user -/// object file is deleted. -/// -/// On macOS the linked binary only carries a debug map pointing at the object -/// files, so `dsymutil` must bake the DWARF into a standalone `.dSYM` bundle -/// while the object still exists. Returns `false` when that fails (the caller -/// then keeps the object file so lldb can follow the debug map instead). -/// On Linux the linker copies `.debug_line` into the binary itself, so there -/// is nothing to do. +/// Bakes DWARF debug info into a standalone `.dSYM` bundle next to `bin_path` +/// via `dsymutil` on macOS (a no-op returning `true` on other platforms). +/// Returns `true` on success (or when nothing needs baking). pub(crate) fn bake_debug_info(target: Target, bin_path: &Path) -> bool { if target.platform != Platform::MacOS { return true; @@ -287,6 +381,44 @@ pub(crate) fn bake_debug_info(target: Target, bin_path: &Path) -> bool { matches!(status, Ok(status) if status.success()) } +/// Returns the `-L` search paths derived from the `ELEPHC_MINGW_SYSROOT` env +/// var for the Windows MinGW link, when that variable is set and points at an +/// existing directory. CI sets it to a cross-built MinGW sysroot containing +/// PE/COFF static archives of PCRE2 (`libpcre2-8.a`, `libpcre2-posix.a`), +/// bzip2 (`libbz2.a`), zlib (`libz.a`), and libiconv (`libiconv.a`), so the +/// `x86_64-w64-mingw32-gcc` link resolves those C symbols. The variable is +/// unset on local non-CI builds, so this returns an empty `Vec` and the link +/// command emits no missing-directory warnings. +/// +/// Both `$SYSROOT/lib` and `$SYSROOT/lib64` are added when present, so a +/// sysroot that installs either layout works without per-lib configuration. +fn mingw_sysroot_link_paths() -> Vec { + let Some(dir) = std::env::var_os("ELEPHC_MINGW_SYSROOT") else { + return Vec::new(); + }; + mingw_sysroot_link_paths_from(&PathBuf::from(dir)) +} + +/// Pure core of [`mingw_sysroot_link_paths`]: returns the `-L` search paths for +/// a given sysroot base directory when it exists, or an empty `Vec` otherwise. +/// Split out so the gating logic can be unit-tested without mutating the +/// process environment (which is racy under parallel test execution). +fn mingw_sysroot_link_paths_from(base: &Path) -> Vec { + if !base.is_dir() { + return Vec::new(); + } + let mut paths = Vec::new(); + let lib = base.join("lib"); + if lib.is_dir() { + paths.push(lib.to_string_lossy().into_owned()); + } + let lib64 = base.join("lib64"); + if lib64.is_dir() { + paths.push(lib64.to_string_lossy().into_owned()); + } + paths +} + /// Links object files and runtime objects into a final binary. /// - `target`: Compiler target (controls platform, linker command, and flags). /// - `emit`: Output kind. `Executable` produces a standalone binary; `Cdylib` @@ -321,7 +453,7 @@ pub(crate) fn link( let needed_bridges: Vec<(&BridgeStaticlib, Option)> = BRIDGES .iter() .filter(|bridge| extra_link_libs.iter().any(|l| l.as_str() == bridge.lib_name)) - .map(|bridge| (bridge, bridge.lib_dir())) + .map(|bridge| (bridge, bridge.lib_dir(target))) .collect(); // A bridge is force-loaded either because its `BRIDGES` entry demands it // (link-time side effects / owned entry point) or because the user passed @@ -396,7 +528,32 @@ pub(crate) fn link( } cmd } - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => { + let mut cmd = crate::windows_toolchain::linker_command() + .unwrap_or_else(|message| fail_tool_configuration("Linker", &message)); + cmd.args(windows_pe_hardening_linker_flags()); + if matches!(emit, Emit::Cdylib) { + cmd.arg("-shared"); + cmd.arg(format!( + "-Wl,--out-implib,{}", + windows_import_library_path(bin_path).display() + )); + } + cmd.arg("-o").arg(bin_path); + cmd.arg(obj_path); + cmd.arg(runtime_object_path); + // Surface a CI-provided MinGW sysroot (cross-built PCRE2, bzip2, + // zlib, libiconv) before the system import libs and any + // `extra_link_libs` (`-lpcre2-8`, `-lbz2`, `-lz`, `-liconv`) so the + // MinGW linker resolves those C symbols against PE/COFF archives + // instead of the ELF dev packages the ubuntu runner also installs. + // Gated on `ELEPHC_MINGW_SYSROOT` so local non-CI builds — which + // never set the env var — see no missing-directory warnings. + for path in mingw_sysroot_link_paths() { + cmd.arg(format!("-L{}", path)); + } + cmd + } }; // Search paths for the located bridge staticlibs. for (_, dir) in &needed_bridges { @@ -426,8 +583,10 @@ pub(crate) fn link( .iter() .find(|(b, d)| { b.lib_name == lib.as_str() - && (b.whole_archive - || forced_whole_archive.iter().any(|l| l.as_str() == b.lib_name)) + && b.requires_whole_archive( + target.platform, + forced_whole_archive.iter().any(|l| l.as_str() == b.lib_name), + ) && d.is_some() }) .map(|(b, _)| (*b, lib.as_str())) @@ -473,9 +632,24 @@ pub(crate) fn link( } dedup_scratch = Some(scratch); } - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => { + } } } + // MinGW Rust staticlibs can each carry std/core/allocator support. In + // addition, rustls-native-certs embeds duplicate import members within the + // TLS archive itself when it is force-loaded. Keep the first identical + // definition only for either of those established COFF cases; ordinary + // single-bridge links retain the linker's duplicate-symbol diagnostics. + if windows_link_needs_duplicate_bridge_tolerance( + target.platform, + needed_bridges.iter().filter(|(_, dir)| dir.is_some()).count(), + whole_archive_order + .iter() + .any(|(bridge, _)| bridge.lib_name == "elephc_tls"), + ) { + ld_cmd.arg("-Wl,--allow-multiple-definition"); + } for lib in extra_link_libs { if lib == "System" { continue; @@ -485,8 +659,10 @@ pub(crate) fn link( // links with a plain `-l`. let whole_archive_bridge = needed_bridges.iter().find(|(bridge, dir)| { bridge.lib_name == lib.as_str() - && (bridge.whole_archive - || forced_whole_archive.iter().any(|l| l.as_str() == bridge.lib_name)) + && bridge.requires_whole_archive( + target.platform, + forced_whole_archive.iter().any(|l| l.as_str() == bridge.lib_name), + ) && dir.is_some() }); match whole_archive_bridge { @@ -506,7 +682,11 @@ pub(crate) fn link( ld_cmd.arg(format!("-l{}", bridge.lib_name)); ld_cmd.arg("-Wl,--no-whole-archive"); } - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => { + ld_cmd.arg("-Wl,--whole-archive"); + ld_cmd.arg(format!("-l{}", bridge.lib_name)); + ld_cmd.arg("-Wl,--no-whole-archive"); + } } } None => { @@ -514,6 +694,17 @@ pub(crate) fn link( } } } + if target.platform == Platform::Windows + && extra_link_libs + .iter() + .any(|library| library == "elephc_magician") + { + // The eval staticlib contains direct PCRE2 and libiconv references. Rust + // staticlib metadata is not propagated to this custom final link, and + // GNU ld scans archives from left to right, so repeat these dependencies + // after the whole-archived magician bridge that introduces the symbols. + ld_cmd.args(windows_magician_transitive_libraries()); + } if target.platform == Platform::Linux && !extra_link_libs.is_empty() { ld_cmd.arg("-Wl,--as-needed"); } @@ -528,6 +719,31 @@ pub(crate) fn link( } } } + if target.platform == Platform::Windows { + // Keep import libraries AFTER every Rust bridge archive. GNU ld scans + // static archives from left to right: placing these before elephc-tz + // left Rust std's Nt*/WSA*/UserEnv references unresolved even though + // the correct MinGW import libraries were named on the command line. + // secur32/userenv/ntdll also cover the std-windows dependencies pulled + // in by the PDO and image bridges. advapi32 resolves the ACL family + // (`OpenThreadToken`/`ImpersonateSelf`/`RevertToSelf`/ + // `GetFileSecurityW`/`MapGenericMask`/`AccessCheck`) the php-parity + // `is_readable()`/`is_writable()` check emits unconditionally + // (`codegen_support::runtime::win32::shims_fs::emit_win_acl_helpers`). + ld_cmd.args([ + "-lkernel32", + "-lmsvcrt", + "-lwinmm", + "-lws2_32", + "-ladvapi32", + "-lbcrypt", + "-lshlwapi", + "-lshell32", + "-lsecur32", + "-luserenv", + "-lntdll", + ]); + } run_tool("Linker", &mut ld_cmd); // The deduped archive copies were only needed for the link command above. if let Some(scratch) = dedup_scratch { @@ -535,6 +751,45 @@ pub(crate) fn link( } } +/// Returns whether MinGW must tolerate identical Rust runtime definitions from +/// multiple located bridge staticlibs. +fn windows_link_needs_duplicate_bridge_tolerance( + platform: Platform, + located_bridge_count: usize, + force_loaded_tls: bool, +) -> bool { + platform == Platform::Windows && (located_bridge_count >= 2 || force_loaded_tls) +} + +/// Returns MinGW libraries that must follow the whole-archived eval bridge. +fn windows_magician_transitive_libraries() -> &'static [&'static str] { + &["-lpcre2-posix", "-lpcre2-8", "-liconv"] +} + +/// Returns the MinGW linker flags that opt generated PE images into supported +/// loader mitigations. +/// +/// GNU ld supports ASLR, 64-bit high-entropy address selection, and DEP through +/// PE DLL-characteristic bits. It does not currently emit a Guard CF load +/// configuration for elephc's hand-written assembly, so CFG is deliberately not +/// advertised here; setting the bit without a valid guard table would be unsafe. +fn windows_pe_hardening_linker_flags() -> &'static [&'static str] { + &[ + "-Wl,--dynamicbase", + "-Wl,--high-entropy-va", + "-Wl,--nxcompat", + ] +} + +/// Returns the conventional MinGW import-library path paired with a Windows DLL. +fn windows_import_library_path(dll_path: &Path) -> PathBuf { + let stem = dll_path + .file_stem() + .and_then(|stem| stem.to_str()) + .unwrap_or("elephc_module"); + dll_path.with_file_name(format!("lib{}.dll.a", stem)) +} + /// Lists the member (object file) names in `archive` via `ar t`. Member-name /// based deduplication does not parse object contents, so it is robust even when /// `nm` cannot read newer-toolchain objects. @@ -675,6 +930,12 @@ fn run_tool(name: &str, cmd: &mut Command) { } } +/// Prints an actionable toolchain configuration error and terminates compilation. +fn fail_tool_configuration(name: &str, message: &str) -> ! { + eprintln!("{name} configuration failed: {message}"); + process::exit(1); +} + /// Returns the macOS SDK path by running `xcrun --show-sdk-path`. /// /// Exits with an actionable diagnostic when no SDK path can be resolved (xcrun missing, @@ -762,6 +1023,24 @@ mod tests { assert_eq!(ok, "/Library/Dev/MacOSX.sdk"); } + /// Verifies GNU Windows bridge output is native only for a GNU Windows + /// compiler process, never for the MSVC host used by native Windows CI. + #[test] + fn windows_gnu_bridge_target_requires_a_matching_host_abi() { + let target = Target::new( + Platform::Windows, + crate::codegen::platform::Arch::X86_64, + ); + assert_eq!( + target_is_native(target), + cfg!(all( + windows, + target_arch = "x86_64", + target_env = "gnu" + )) + ); + } + /// Verifies the elephc-crypto bridge is registered and produces the expected /// archive filename, so compiled programs that use hashing can link it. #[test] @@ -836,6 +1115,55 @@ mod tests { assert_eq!(bridge_lib_for_flag("web"), Some("elephc_web")); } + /// Verifies the web bridge is force-loaded on Unix targets that need its + /// entry-point machinery. + #[test] + fn web_bridge_force_loads_on_linux_and_macos() { + let bridge = BRIDGES + .iter() + .find(|bridge| bridge.lib_name == "elephc_web") + .expect("web bridge entry"); + assert!(bridge.requires_whole_archive(Platform::Linux, false)); + assert!(bridge.requires_whole_archive(Platform::MacOS, false)); + } + + /// Verifies the web bridge remains a plain archive link on Windows so GNU/COFF + /// linking does not force-load duplicate import members from its Rust staticlib. + #[test] + fn web_bridge_does_not_force_whole_archive_on_windows() { + let bridge = BRIDGES + .iter() + .find(|bridge| bridge.lib_name == "elephc_web") + .expect("web bridge entry"); + assert!(!bridge.requires_whole_archive(Platform::Windows, false)); + } + + /// Verifies MinGW tolerates duplicate bridge runtime members and the + /// duplicate import members introduced by a force-loaded TLS archive. + #[test] + fn windows_duplicate_bridge_or_tls_import_members_enable_tolerance() { + assert!(!windows_link_needs_duplicate_bridge_tolerance( + Platform::Windows, + 1, + false, + )); + assert!(windows_link_needs_duplicate_bridge_tolerance( + Platform::Windows, + 2, + false, + )); + assert!(windows_link_needs_duplicate_bridge_tolerance( + Platform::Windows, + 1, + true, + )); + assert!(!windows_link_needs_duplicate_bridge_tolerance( + Platform::Linux, + 2, + true, + )); + } + /// Verifies an unknown crate flag resolves to `None` so the CLI rejects /// `--with-` instead of silently ignoring it. #[test] @@ -861,7 +1189,14 @@ mod tests { .find(|b| b.lib_name == "elephc_magician") .expect("elephc_magician must be a registered bridge"); - let resolved = entry.lib_dir(); + let resolved = entry.lib_dir(Target::new( + Platform::detect_host(), + if cfg!(target_arch = "aarch64") { + crate::codegen::platform::Arch::AArch64 + } else { + crate::codegen::platform::Arch::X86_64 + }, + )); match previous { Some(value) => std::env::set_var("ELEPHC_MAGICIAN_LIB_DIR", value), @@ -869,4 +1204,111 @@ mod tests { } assert_eq!(resolved.as_deref(), Some(override_dir)); } + + /// Verifies Windows cross-discovery ignores a same-named host archive in + /// `CARGO_TARGET_DIR/debug` and selects the PE/COFF archive under Cargo's + /// target-triple directory instead. + #[test] + fn windows_bridge_discovery_prefers_cross_target_archive() { + let _guard = ENV_LOCK + .get_or_init(|| Mutex::new(())) + .lock() + .expect("env lock should not be poisoned"); + let previous_target_dir = std::env::var_os("CARGO_TARGET_DIR"); + let previous_override = std::env::var_os("ELEPHC_TZ_LIB_DIR"); + let tmp = std::env::temp_dir().join(format!( + "elephc-bridge-target-discovery-{}", + std::process::id() + )); + let host_dir = tmp.join("debug"); + let coff_dir = tmp.join("x86_64-pc-windows-gnu").join("debug"); + std::fs::create_dir_all(&host_dir).expect("create host archive directory"); + std::fs::create_dir_all(&coff_dir).expect("create COFF archive directory"); + std::fs::write(host_dir.join("libelephc_tz.a"), b"host").expect("write host archive"); + std::fs::write(coff_dir.join("libelephc_tz.a"), b"coff").expect("write COFF archive"); + std::env::set_var("CARGO_TARGET_DIR", &tmp); + std::env::remove_var("ELEPHC_TZ_LIB_DIR"); + + let entry = BRIDGES + .iter() + .find(|bridge| bridge.lib_name == "elephc_tz") + .expect("elephc_tz must be registered"); + let resolved = entry.find_lib_dir(Target::new( + Platform::Windows, + crate::codegen::platform::Arch::X86_64, + )); + + match previous_target_dir { + Some(value) => std::env::set_var("CARGO_TARGET_DIR", value), + None => std::env::remove_var("CARGO_TARGET_DIR"), + } + match previous_override { + Some(value) => std::env::set_var("ELEPHC_TZ_LIB_DIR", value), + None => std::env::remove_var("ELEPHC_TZ_LIB_DIR"), + } + let _ = std::fs::remove_dir_all(&tmp); + + assert_eq!(resolved.as_deref(), Some(coff_dir.to_string_lossy().as_ref())); + } + + /// Verifies a non-existent sysroot base produces no search paths, so a + /// stray `ELEPHC_MINGW_SYSROOT` value can never emit a missing-directory + /// linker warning. + #[test] + fn mingw_sysroot_paths_empty_for_missing_dir() { + let paths = mingw_sysroot_link_paths_from(Path::new("/nonexistent/elephc-mingw-sysroot-123")); + assert!(paths.is_empty(), "got: {paths:?}"); + } + + /// Verifies a real sysroot with a `lib` directory is surfaced as a `-L` + /// path, and that `lib64` is also added when present, so a CI cross-built + /// sysroot is picked up regardless of which layout the libs installed into. + #[test] + fn mingw_sysroot_paths_from_real_dir() { + let tmp = std::env::temp_dir().join(format!("elephc-mingw-sysroot-test-{}", std::process::id())); + std::fs::remove_dir_all(&tmp).ok(); + std::fs::create_dir_all(tmp.join("lib")).unwrap(); + std::fs::create_dir_all(tmp.join("lib64")).unwrap(); + let paths = mingw_sysroot_link_paths_from(&tmp); + assert_eq!(paths.len(), 2); + assert!(paths[0].ends_with("lib"), "got: {paths:?}"); + assert!(paths[1].ends_with("lib64"), "got: {paths:?}"); + std::fs::remove_dir_all(&tmp).ok(); + } + + /// Verifies only `lib` is returned when `lib64` is absent, so sysroots + /// that install solely into `lib` do not produce a phantom `lib64` entry. + #[test] + fn mingw_sysroot_paths_lib_only() { + let tmp = std::env::temp_dir().join(format!("elephc-mingw-sysroot-lib-only-{}", std::process::id())); + std::fs::remove_dir_all(&tmp).ok(); + std::fs::create_dir_all(tmp.join("lib")).unwrap(); + let paths = mingw_sysroot_link_paths_from(&tmp); + assert_eq!(paths.len(), 1); + assert!(paths[0].ends_with("lib"), "got: {paths:?}"); + std::fs::remove_dir_all(&tmp).ok(); + } + + /// Verifies Windows PE links explicitly request every loader mitigation + /// supported by the MinGW linker instead of relying on toolchain defaults. + #[test] + fn windows_pe_hardening_flags_enable_aslr_dep_and_high_entropy() { + assert_eq!( + windows_pe_hardening_linker_flags(), + &[ + "-Wl,--dynamicbase", + "-Wl,--high-entropy-va", + "-Wl,--nxcompat", + ] + ); + } + + /// Verifies the eval bridge's native dependencies retain GNU archive scan order. + #[test] + fn windows_magician_dependencies_follow_the_bridge() { + assert_eq!( + windows_magician_transitive_libraries(), + &["-lpcre2-posix", "-lpcre2-8", "-liconv"] + ); + } } diff --git a/src/magic_constants/file_pass.rs b/src/magic_constants/file_pass.rs index 5b4f71adb5..85df6f136f 100644 --- a/src/magic_constants/file_pass.rs +++ b/src/magic_constants/file_pass.rs @@ -25,13 +25,11 @@ use super::walker::{walk_program, Pass}; /// /// Note: The file path is canonicalized before conversion to a string to resolve symlinks and relative paths. pub(super) fn substitute_file_constants(stmts: Vec, file_path: &Path) -> Vec { - let canonical = file_path - .canonicalize() - .unwrap_or_else(|_| file_path.to_path_buf()); - let file = canonical.display().to_string(); + let canonical = file_path.canonicalize().unwrap_or_else(|_| file_path.to_path_buf()); + let file = crate::source_path::source_path_display(&canonical); let dir = canonical .parent() - .map(|p| p.display().to_string()) + .map(crate::source_path::source_path_display) .unwrap_or_default(); let mut pass = FilePass { file, dir }; walk_program(stmts, &mut pass) diff --git a/src/magic_constants/scope_pass.rs b/src/magic_constants/scope_pass.rs index 82db07620a..9c3f216071 100644 --- a/src/magic_constants/scope_pass.rs +++ b/src/magic_constants/scope_pass.rs @@ -22,10 +22,10 @@ use super::{namespace_string, qualify, TRAIT_CLASS_PLACEHOLDER}; /// /// Canonicalizes the file path and delegates to `substitute_scope_constants_with_file`. pub(super) fn substitute_scope_constants_in_file(program: Program, file_path: &Path) -> Program { - let canonical = file_path - .canonicalize() - .unwrap_or_else(|_| file_path.to_path_buf()); - substitute_scope_constants_with_file(program, Some(canonical.display().to_string())) + substitute_scope_constants_with_file( + program, + Some(crate::source_path::canonical_source_path(file_path)), + ) } /// Inner implementation that accepts an optional already-canonicalized file string. diff --git a/src/main.rs b/src/main.rs index 6a65bf735a..4df45e90e8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,6 +40,7 @@ mod resolver; mod runtime_cache; mod debug_info; mod source_map; +mod source_path; mod span; mod strict_php; mod string_bytes; @@ -50,12 +51,45 @@ mod types; mod tz_prelude; mod var_export_prelude; mod web_prelude; +mod windows_toolchain; -/// Entry point for the `elephc` binary. +/// Runs the compiler entry point on the platform's main or a dedicated worker thread. /// -/// Collects command-line arguments, parses them into a `Config`, and delegates -/// to the compile pipeline. Exits via `std::process::exit` if compilation fails -/// (the pipeline handles fatal error reporting internally). +/// Windows gives the executable's initial thread a comparatively small fixed +/// stack. The compiler still contains recursive frontend and lowering paths, +/// so Windows moves that work to an explicitly sized Rust thread. This leaves +/// the process entry thread shallow while preserving the normal process-wide +/// exit behavior used by fatal compilation diagnostics. +/// +/// # Inputs +/// - The operating-system process entry point. +/// +/// # Outputs +/// - Returns once the compiler succeeds, or propagates a worker panic. +/// +/// # Side effects +/// - Allocates a Windows compiler worker stack before invoking the pipeline. +fn main() { + #[cfg(windows)] + { + const WINDOWS_COMPILER_STACK_BYTES: usize = 64 * 1024 * 1024; + std::thread::Builder::new() + .name("elephc-compiler".into()) + .stack_size(WINDOWS_COMPILER_STACK_BYTES) + .spawn(run_compiler) + .expect("create Windows compiler worker thread") + .join() + .expect("Windows compiler worker thread panicked"); + } + + #[cfg(not(windows))] + run_compiler(); +} + +/// Parses CLI arguments and runs the compiler's ordered pipeline. +/// +/// A Windows caller invokes this from a dedicated large-stack worker because +/// `RUST_MIN_STACK` does not resize the executable's original main thread. /// /// # Inputs /// - `std::env::args()`: OS-provided arguments, where `args[0]` is the program name. @@ -68,7 +102,7 @@ mod web_prelude; /// - Reads source files and writes the compiled binary alongside the source. /// - Emits warnings/errors to stderr. /// - May create temporary files during assembly and linking. -fn main() { +fn run_compiler() { let args: Vec = std::env::args().collect(); if cli::wants_mascotte(&args) { cli::print_mascotte(); diff --git a/src/name_resolver/mod.rs b/src/name_resolver/mod.rs index 2f3022ff41..a738e14031 100644 --- a/src/name_resolver/mod.rs +++ b/src/name_resolver/mod.rs @@ -16,6 +16,7 @@ mod symbols; use std::collections::{HashMap, HashSet}; +use crate::codegen::platform::Platform; use crate::errors::CompileError; use crate::names::{Name, NameKind}; use crate::parser::ast::{Expr, ExprKind, Program}; @@ -31,7 +32,6 @@ struct Imports { /// Internal symbol table for tracking declared functions, classes, interfaces, traits, /// constants, and extern symbols within a namespace scope. -#[derive(Default)] struct Symbols { functions: HashMap, classes: HashMap, @@ -40,11 +40,21 @@ struct Symbols { constants: HashSet, extern_functions: HashMap, extern_classes: HashMap, + platform: Platform, } -/// Resolves PHP namespace/use statements and rewrites names to canonical forms across the program. +/// Resolves names for the host platform. +#[allow(dead_code)] pub fn resolve(program: Program) -> Result { - let mut symbols = Symbols::default(); + resolve_for_platform(program, Platform::detect_host()) +} + +/// Resolves PHP namespace/use statements using the selected target's builtin surface. +pub fn resolve_for_platform( + program: Program, + platform: Platform, +) -> Result { + let mut symbols = Symbols::new(platform); symbols::collect_symbols(&program, None, &mut symbols); statements::resolve_stmt_list(&program, None, &Imports::default(), &symbols) } @@ -132,6 +142,12 @@ pub(crate) fn canonical_builtin_function_name(name: &str) -> Option { crate::types::checker::builtins::canonical_builtin_function_name(name) } +/// Returns whether `name` is a builtin available on the selected target platform. +pub(crate) fn is_builtin_function_on_platform(name: &str, platform: Platform) -> bool { + crate::types::checker::builtins::canonical_builtin_function_name_on_platform(name, platform) + .is_some() +} + /// Reports whether `name` matches one of PHP's procedural date/time aliases /// (e.g. `date_create`, `idate`, `gmstrftime`). The name set is the same as the one /// rewritten by `expressions::rewrite_date_procedural_alias`, minus the per-arity guards, @@ -148,3 +164,35 @@ pub(crate) fn is_date_procedural_alias(name: &str) -> bool { pub(crate) fn date_procedural_alias_arity(name: &str) -> Option<(usize, usize)> { expressions::date_procedural_alias_arity(name) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::codegen::platform::Platform; + use crate::parser::ast::{CallableTarget, StmtKind}; + + /// Verifies namespaced calls only fall back to `lchown` where the target exposes it. + #[test] + fn namespaced_lchown_fallback_is_target_aware() { + let tokens = crate::lexer::tokenize(" bool { - if matches!( - name, - "PHP_OS" - | "SID" - | "PATHINFO_DIRNAME" - | "PATHINFO_BASENAME" - | "PATHINFO_EXTENSION" - | "PATHINFO_FILENAME" - | "PATHINFO_ALL" - | "FNM_NOESCAPE" - | "FNM_PATHNAME" - | "FNM_PERIOD" - | "FNM_CASEFOLD" - | "ARRAY_FILTER_USE_VALUE" - | "ARRAY_FILTER_USE_BOTH" - | "ARRAY_FILTER_USE_KEY" - | "STDIN" - | "STDOUT" - | "STDERR" - | "PHP_INT_MAX" - | "PHP_INT_MIN" - | "PHP_FLOAT_MAX" - | "PHP_FLOAT_MIN" - | "PHP_FLOAT_EPSILON" - | "INF" - | "NAN" - | "M_PI" - | "M_E" - | "M_SQRT2" - | "M_PI_2" - | "M_PI_4" - | "M_LOG2E" - | "M_LOG10E" - | "PHP_EOL" - | "DIRECTORY_SEPARATOR" - ) { - return true; - } - // Shared source-of-truth slices for JSON, stream/socket, and session constants. + if matches!( + name, + "PHP_OS" + | "PHP_OS_FAMILY" + | "SID" + | "PHP_EOL" + | "DIRECTORY_SEPARATOR" + | "PATH_SEPARATOR" + | "PHP_INT_SIZE" + | "PHP_VERSION" + | "PHP_MAJOR_VERSION" + | "PHP_MINOR_VERSION" + | "PHP_RELEASE_VERSION" + | "PHP_VERSION_ID" + | "PHP_EXTRA_VERSION" + | "PATHINFO_DIRNAME" + | "PATHINFO_BASENAME" + | "PATHINFO_EXTENSION" + | "PATHINFO_FILENAME" + | "PATHINFO_ALL" + | "FNM_NOESCAPE" + | "FNM_PATHNAME" + | "FNM_PERIOD" + | "FNM_CASEFOLD" + | "ARRAY_FILTER_USE_VALUE" + | "ARRAY_FILTER_USE_BOTH" + | "ARRAY_FILTER_USE_KEY" + | "STDIN" + | "STDOUT" + | "STDERR" + | "PHP_INT_MAX" + | "PHP_INT_MIN" + | "PHP_FLOAT_MAX" + | "PHP_FLOAT_MIN" + | "PHP_FLOAT_EPSILON" + | "INF" + | "NAN" + | "M_PI" + | "M_E" + | "M_SQRT2" + | "M_PI_2" + | "M_PI_4" + | "M_LOG2E" + | "M_LOG10E" + ) { + return true; + } + // Shared source-of-truth slices for JSON, stream/socket, session, and error constants. crate::types::json_constants::JSON_INT_CONSTANTS .iter() .chain(crate::types::stream_constants::STREAM_INT_CONSTANTS.iter()) diff --git a/src/name_resolver/symbols.rs b/src/name_resolver/symbols.rs index 1684386cae..fd272599e0 100644 --- a/src/name_resolver/symbols.rs +++ b/src/name_resolver/symbols.rs @@ -11,7 +11,7 @@ use crate::names::{canonical_name_for_decl, php_symbol_key}; use crate::parser::ast::{Stmt, StmtKind}; -use super::{canonical_builtin_function_name, namespace_name, Symbols}; +use super::{namespace_name, Symbols}; const BUILTIN_CLASS_LIKE_SYMBOLS: &[&str] = &[ "ArrayAccess", @@ -54,14 +54,33 @@ const BUILTIN_CLASS_LIKE_SYMBOLS: &[&str] = &[ ]; impl Symbols { - /// canonical_function + /// Creates an empty symbol table tied to the target platform's PHP builtin surface. + pub(super) fn new(platform: crate::codegen::platform::Platform) -> Self { + Self { + functions: Default::default(), + classes: Default::default(), + interfaces: Default::default(), + traits: Default::default(), + constants: Default::default(), + extern_functions: Default::default(), + extern_classes: Default::default(), + platform, + } + } + + /// Resolves a declared, extern, or target-available builtin function name. pub(super) fn canonical_function(&self, name: &str) -> Option { let key = php_symbol_key(name); self.functions .get(&key) .or_else(|| self.extern_functions.get(&key)) .cloned() - .or_else(|| canonical_builtin_function_name(name)) + .or_else(|| { + crate::types::checker::builtins::canonical_builtin_function_name_on_platform( + name, + self.platform, + ) + }) } /// Returns whether `name` resolves to a user-declared (or extern) function, diff --git a/src/optimize/control/dce.rs b/src/optimize/control/dce.rs index 19da708a61..877068df5d 100644 --- a/src/optimize/control/dce.rs +++ b/src/optimize/control/dce.rs @@ -611,7 +611,7 @@ fn dce_stmt_with_guards(stmt: Stmt, guards: &GuardState) -> Vec { } StmtKind::ExprStmt(expr) => { let expr = prune_expr(expr); - if expr_has_side_effects(&expr) { + if expr_is_observable(&expr) { vec![Stmt { kind: StmtKind::ExprStmt(expr), span, diff --git a/src/optimize/control/prune.rs b/src/optimize/control/prune.rs index 6b10057be4..da64302b64 100644 --- a/src/optimize/control/prune.rs +++ b/src/optimize/control/prune.rs @@ -13,6 +13,6 @@ mod loop_exit; mod statements; pub(crate) use expr::{ - callable_target_effect, expr_has_side_effects, prune_expr, + callable_target_effect, prune_expr, }; pub(crate) use statements::prune_block; diff --git a/src/optimize/control/prune/statements.rs b/src/optimize/control/prune/statements.rs index 3ed1892269..5d8cfe0f42 100644 --- a/src/optimize/control/prune/statements.rs +++ b/src/optimize/control/prune/statements.rs @@ -9,7 +9,7 @@ //! - Loop exits, empty bodies, and effectful conditions must be handled before removing structural statements. use super::super::*; -use super::expr::{expr_has_side_effects, prune_expr}; +use super::expr::prune_expr; use super::loop_exit::block_contains_loop_exit; /// Recursively processes a block of statements, pruning each one and stopping early @@ -306,7 +306,7 @@ pub(crate) fn prune_stmt(stmt: Stmt) -> Vec { } StmtKind::ExprStmt(expr) => { let expr = prune_expr(expr); - if expr_has_side_effects(&expr) { + if expr_is_observable(&expr) { vec![Stmt { kind: StmtKind::ExprStmt(expr), span, diff --git a/src/optimize/effects.rs b/src/optimize/effects.rs index fb1fe5c9f3..44ff0ae14b 100644 --- a/src/optimize/effects.rs +++ b/src/optimize/effects.rs @@ -198,6 +198,13 @@ pub(super) fn expr_is_observable(expr: &Expr) -> bool { expr_effect(expr).is_observable() } +/// Returns whether an expression mutates or outputs without considering a +/// catchable exception edge; pruning uses this narrower classification locally. +#[cfg(test)] +pub(crate) fn expr_has_side_effects(expr: &Expr) -> bool { + expr_effect(expr).has_side_effects +} + /// Computes the combined `Effect` for an expression, including all sub-expressions and call effects. /// Covers all `ExprKind` variants, classifying reads, writes, calls, throws, output, and runtime-state interactions. /// For `MagicConstant`, returns `unreachable!` because they must be lowered before optimizer passes. diff --git a/src/optimize/effects/calls.rs b/src/optimize/effects/calls.rs index f6dff38a70..2c4bc196a0 100644 --- a/src/optimize/effects/calls.rs +++ b/src/optimize/effects/calls.rs @@ -16,13 +16,14 @@ use crate::types::PhpType; /// Uses thread-local `ACTIVE_FUNCTION_EFFECTS` for user-defined functions. Falls back to /// Registry builtins consume their shared descriptor; unknown calls remain conservative. pub(in crate::optimize) fn function_call_effect(name: &str, args: &[Expr]) -> Effect { + let normalized_name = name.trim_start_matches('\\'); ACTIVE_FUNCTION_EFFECTS.with(|slot| { slot.borrow() .as_ref() - .and_then(|effects| effects.get(name).copied()) + .and_then(|effects| effects.get(normalized_name).copied()) }) .unwrap_or_else(|| { - if let Some(def) = crate::builtins::registry::lookup(name) { + if let Some(def) = crate::builtins::registry::lookup(normalized_name) { let semantics = def.spec.semantics; let arg_types = semantic_optimizer_arg_types(def, args); let input = crate::builtins::semantics::BuiltinSemanticInput { @@ -36,7 +37,7 @@ pub(in crate::optimize) fn function_call_effect(name: &str, args: &[Expr]) -> Ef crate::builtins::semantics::BuiltinEffects::Shared(resolve) => resolve(&input), }; let effect = Effect::from_eir(effects); - if builtin_invokes_callbacks(name) { + if builtin_invokes_callbacks(normalized_name) { return effect.with_side_effects().with_may_throw().with_writes_globals(); } effect diff --git a/src/optimize/tests/dce/tries/try_pruning.rs b/src/optimize/tests/dce/tries/try_pruning.rs index f7c2a0e016..39bade854e 100644 --- a/src/optimize/tests/dce/tries/try_pruning.rs +++ b/src/optimize/tests/dce/tries/try_pruning.rs @@ -70,7 +70,7 @@ fn test_eliminate_dead_code_drops_statements_after_exhaustive_try_catch() { } /// Verifies that DCE removes an empty try-catch shell when both bodies contain only pure (side-effect-free) statements. -/// After eliminating the dead pure bodies, the try-catch itself becomes a no-op and is removed, leaving an empty function. +/// DCE first removes the pure bodies, then drops the now-empty try shell, leaving an empty function. #[test] fn test_eliminate_dead_code_drops_empty_try_shell_created_by_branch_dce() { let pure_builtin = Expr::new( @@ -114,6 +114,61 @@ fn test_eliminate_dead_code_drops_empty_try_shell_created_by_branch_dce() { assert!(body.is_empty()); } +/// Verifies that a fully qualified builtin carrying `MAY_THROW` keeps its +/// catch path, even when the builtin result itself is unused. +#[test] +fn test_eliminate_dead_code_keeps_fqn_throwing_builtin_try_catch() { + let throwing_call = Expr::new( + ExprKind::FunctionCall { + name: Name::from_parts( + crate::names::NameKind::FullyQualified, + vec!["escapeshellarg".into()], + ), + args: vec![Expr::string_lit("value")], + }, + Span::dummy(), + ); + assert!(expr_effect(&throwing_call).may_throw); + let program = vec![Stmt::new( + StmtKind::FunctionDecl { + name: "main".into(), + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: None, + by_ref_return: false, + body: vec![Stmt::new( + StmtKind::Try { + try_body: vec![Stmt::new( + StmtKind::ExprStmt(throwing_call), + Span::dummy(), + )], + catches: vec![crate::parser::ast::CatchClause { + exception_types: vec!["ValueError".into()], + variable: Some("error".into()), + body: vec![Stmt::echo(Expr::string_lit("caught"))], + }], + finally_body: None, + }, + Span::dummy(), + )], + }, + Span::dummy(), + )]; + + let eliminated = eliminate_dead_code(program); + + let StmtKind::FunctionDecl { body, .. } = &eliminated[0].kind else { + panic!("expected function"); + }; + let StmtKind::Try { catches, .. } = &body[0].kind else { + panic!("expected throwing try/catch to survive dead-code elimination"); + }; + assert_eq!(catches.len(), 1); +} + /// Verifies that an unknown truthy switch entry is preserved before a matching case. /// When the switch subject (`flag`) is unknown at compile time, a truthy guard in a case pattern must not be pruned, /// because the case list contains an unknown variable (`other`) that may be truthy at runtime. diff --git a/src/parser/expr/prefix.rs b/src/parser/expr/prefix.rs index cd8499ebca..145e385de7 100644 --- a/src/parser/expr/prefix.rs +++ b/src/parser/expr/prefix.rs @@ -128,7 +128,12 @@ pub(super) fn parse_prefix( span, ExprKind::ConstRef(Name::unqualified("STDERR")), ), - Token::PhpEol => parse_simple(tokens, pos, span, ExprKind::StringLiteral("\n".to_string())), + Token::PhpEol => parse_simple( + tokens, + pos, + span, + ExprKind::ConstRef(Name::unqualified("PHP_EOL")), + ), Token::PhpOs => parse_simple( tokens, pos, @@ -139,7 +144,13 @@ pub(super) fn parse_prefix( tokens, pos, span, - ExprKind::StringLiteral("/".to_string()), + ExprKind::ConstRef(Name::unqualified("DIRECTORY_SEPARATOR")), + ), + Token::PathSeparator => parse_simple( + tokens, + pos, + span, + ExprKind::ConstRef(Name::unqualified("PATH_SEPARATOR")), ), Token::DunderLine => { parse_simple(tokens, pos, span, ExprKind::IntLiteral(span.line as i64)) diff --git a/src/parser/stmt/namespace_use.rs b/src/parser/stmt/namespace_use.rs index 2050d79c23..09e8c69b6b 100644 --- a/src/parser/stmt/namespace_use.rs +++ b/src/parser/stmt/namespace_use.rs @@ -292,7 +292,8 @@ fn token_as_import_name(token: &Token, metadata: &crate::lexer::TokenMetadata) - | Token::Stderr | Token::PhpEol | Token::PhpOs - | Token::DirectorySeparator => token.word_spelling(metadata).map(str::to_string), + | Token::DirectorySeparator + | Token::PathSeparator => token.word_spelling(metadata).map(str::to_string), _ => None, } } diff --git a/src/pipeline.rs b/src/pipeline.rs index 99c0a7579e..ffbbd40d03 100644 --- a/src/pipeline.rs +++ b/src/pipeline.rs @@ -202,7 +202,7 @@ pub(crate) fn compile(config: CliConfig) { crate::progress::phase("name-resolve"); let phase_started = Instant::now(); - let ast = match name_resolver::resolve(ast) { + let ast = match name_resolver::resolve_for_platform(ast, target.platform) { Ok(resolved) => resolved, Err(e) => { crate::progress::clear(); @@ -214,7 +214,7 @@ pub(crate) fn compile(config: CliConfig) { crate::progress::phase("autoload-run"); let phase_started = Instant::now(); - let ast = match autoload::run(ast, parent, &autoload_registry) { + let ast = match autoload::run_for_platform(ast, parent, &autoload_registry, target.platform) { Ok(resolved) => resolved, Err(e) => { crate::progress::clear(); @@ -372,6 +372,19 @@ pub(crate) fn compile(config: CliConfig) { // web and non-web runtime objects distinct automatically. runtime_features.web = web; + // The windows-x86_64 third-party ABI shims (zlib/bzip2/pcre2/iconv) `call` + // real library symbols, so emit them only when the program actually links + // that library. Derived from the same `required_libraries` signal that adds + // `-lz`/`-lbz2`/`-liconv`, so shim emission and linking stay consistent; the + // runtime cache (keyed on the assembly hash) distinguishes the objects + // automatically. `regex` already gates the pcre2 shims. + let program_requires_lib = |name: &str| { + check_result.required_libraries.iter().any(|lib| lib == name) + }; + runtime_features.zlib = program_requires_lib("z"); + runtime_features.bzip2 = program_requires_lib("bz2"); + runtime_features.iconv = program_requires_lib("iconv"); + if web && !extra_link_libs.iter().any(|lib| lib == "elephc_web") { extra_link_libs.push("elephc_web".to_string()); } @@ -395,6 +408,7 @@ pub(crate) fn compile(config: CliConfig) { .required_libraries .iter() .any(|lib| lib == "elephc_tls"); + runtime_features.tls = requires_elephc_tls; crate::progress::phase("runtime-cache"); let phase_started = Instant::now(); @@ -429,11 +443,17 @@ pub(crate) fn compile(config: CliConfig) { process::exit(1); } }; - let user_asm = if emit_debug_info { + let mut user_asm = if emit_debug_info { debug_info::inject_line_directives(&user_asm, filename, target.platform) } else { user_asm }; + // On Windows, rewrite the raw Linux `mov eax, N; syscall` sequences shared by + // the x86_64 backend into `call __rt_sys_` shim calls before the assembly + // is written, source-mapped, or assembled. Linux/macOS assembly is untouched. + if target.platform == Platform::Windows { + user_asm = codegen::platform::transform_for_windows(&user_asm); + } timings.record_since("codegen", phase_started); for lib in &check_result.required_libraries { @@ -539,8 +559,8 @@ pub(crate) fn compile(config: CliConfig) { /// derived from the input filename. /// /// Executable mode produces `` (no extension). Cdylib mode produces -/// `lib.so` (Linux) or `lib.dylib` (macOS), matching the conventional -/// shared-library naming that `dlopen(3)` and linker `-l` flags expect. +/// `lib.so` (Linux), `lib.dylib` (macOS), or `.dll` (Windows), +/// matching each platform's conventional loadable-library naming. fn output_paths(filename: &str, target: Target, emit: Emit) -> OutputPaths { let path = Path::new(filename); let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("output"); @@ -550,7 +570,7 @@ fn output_paths(filename: &str, target: Target, emit: Emit) -> OutputPaths { Emit::Cdylib => match target.platform { Platform::MacOS => format!("lib{}.dylib", stem), Platform::Linux => format!("lib{}.so", stem), - Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + Platform::Windows => format!("{}.dll", stem), }, }; OutputPaths { diff --git a/src/runtime_cache.rs b/src/runtime_cache.rs index c850de576a..8e75fd34aa 100644 --- a/src/runtime_cache.rs +++ b/src/runtime_cache.rs @@ -59,10 +59,16 @@ pub fn prepare_runtime_object( fs::create_dir_all(&cache_dir) .map_err(|err| format!("failed to create runtime cache '{}': {}", cache_dir.display(), err))?; - let runtime_asm = + let mut runtime_asm = codegen::generate_runtime_with_features_pic(heap_size, target, features, pic); + // On Windows, rewrite the shared x86_64 runtime's raw `mov eax, N; syscall` + // sequences into `call __rt_sys_` shim calls before hashing, so the cache + // key reflects the final assembled bytes. Linux/macOS runtime asm is untouched. + if target.platform == Platform::Windows { + runtime_asm = codegen::platform::transform_for_windows(&runtime_asm); + } let runtime_hash = runtime_asm_hash(&runtime_asm); - let cache_path = cache_dir.join(runtime_cache_file_name(heap_size, target, runtime_hash)); + let cache_path = cache_dir.join(runtime_cache_file_name(heap_size, target, runtime_hash)?); if cache_path.exists() { return Ok(PreparedRuntimeObject { path: cache_path, @@ -92,35 +98,47 @@ pub fn prepare_runtime_object( ) })?; - let mut assembler = Command::new(target.assembler_cmd()); - if target.platform == Platform::MacOS { - assembler.args(["-arch", target.darwin_arch_name()]); - } - assembler.arg("-o").arg(&temp_obj_path).arg(&temp_asm_path); + let mut assembler = if target.platform == Platform::Windows { + crate::windows_toolchain::assembler_command(&temp_asm_path, &temp_obj_path)? + } else { + let mut command = Command::new(target.assembler_cmd()); + if target.platform == Platform::MacOS { + command.args(["-arch", target.darwin_arch_name()]); + } + command.arg("-o").arg(&temp_obj_path).arg(&temp_asm_path); + command + }; + let assembler_name = assembler.get_program().to_string_lossy().into_owned(); let assembler_status = assembler.status().map_err(|err| { format!( "failed to run runtime assembler '{}' for '{}': {}", - target.assembler_cmd(), + assembler_name, temp_obj_path.display(), err ) })?; - let _ = fs::remove_file(&temp_asm_path); if !assembler_status.success() { let _ = fs::remove_file(&temp_obj_path); return Err(format!( - "runtime assembler failed while building '{}'", - cache_path.display() + "runtime assembler failed while building '{}' (asm left at {})", + cache_path.display(), + temp_asm_path.display() )); } match fs::rename(&temp_obj_path, &cache_path) { - Ok(()) => Ok(PreparedRuntimeObject { - path: cache_path, - status: RuntimeCacheStatus::Miss, - }), + Ok(()) => { + // Object is safely in place — remove the temporary assembly source. + let _ = fs::remove_file(&temp_asm_path); + Ok(PreparedRuntimeObject { + path: cache_path, + status: RuntimeCacheStatus::Miss, + }) + } Err(_err) if cache_path.exists() => { + // A concurrent run already produced the object — drop our temporaries. let _ = fs::remove_file(&temp_obj_path); + let _ = fs::remove_file(&temp_asm_path); Ok(PreparedRuntimeObject { path: cache_path, status: RuntimeCacheStatus::Hit, @@ -149,14 +167,30 @@ fn runtime_cache_dir() -> PathBuf { } /// Builds the cache file name for a runtime object. -fn runtime_cache_file_name(heap_size: usize, target: Target, runtime_hash: u64) -> String { - format!( - "runtime-v{}-{}-rt{:016x}-heap{}.o", - env!("CARGO_PKG_VERSION"), - target.as_str(), - runtime_hash, - heap_size - ) +fn runtime_cache_file_name( + heap_size: usize, + target: Target, + runtime_hash: u64, +) -> Result { + if target.platform == Platform::Windows { + let toolchain = crate::windows_toolchain::WindowsToolchain::configured()?.cache_key(); + Ok(format!( + "runtime-v{}-{}-{}-rt{:016x}-heap{}.o", + env!("CARGO_PKG_VERSION"), + target.as_str(), + toolchain, + runtime_hash, + heap_size + )) + } else { + Ok(format!( + "runtime-v{}-{}-rt{:016x}-heap{}.o", + env!("CARGO_PKG_VERSION"), + target.as_str(), + runtime_hash, + heap_size + )) + } } /// Computes a 64-bit FNV-1a hash of the given assembly string. diff --git a/src/source_path.rs b/src/source_path.rs new file mode 100644 index 0000000000..5028f7f509 --- /dev/null +++ b/src/source_path.rs @@ -0,0 +1,73 @@ +//! Purpose: +//! Normalizes compiler source paths before they become PHP-visible strings or +//! EIR metadata. +//! +//! Called from: +//! - Magic-constant substitution for `__FILE__`, `__DIR__`, and closure names. +//! - EIR module construction for source-file metadata. +//! +//! Key details: +//! - Windows `std::fs::canonicalize` may add a verbatim `\\?\` prefix. PHP +//! exposes ordinary drive or UNC paths, and verbatim paths reject the `/` +//! separators commonly concatenated onto `__DIR__`. + +use std::path::Path; + +/// Canonicalizes a source path when possible and returns its PHP-display form. +pub(crate) fn canonical_source_path(path: &Path) -> String { + let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); + source_path_display(&canonical) +} + +/// Converts a path to a display string without Windows' internal verbatim +/// prefix, preserving ordinary drive-letter and UNC syntax. +pub(crate) fn source_path_display(path: &Path) -> String { + let display = path.to_string_lossy(); + if cfg!(windows) { + strip_windows_verbatim_prefix(&display) + } else { + display.into_owned() + } +} + +/// Removes a Windows verbatim drive or UNC prefix from a display string. +fn strip_windows_verbatim_prefix(path: &str) -> String { + if let Some(rest) = path.strip_prefix(r"\\?\UNC\") { + return format!(r"\\{rest}"); + } + path.strip_prefix(r"\\?\").unwrap_or(path).to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Verifies drive-letter canonical paths are made suitable for PHP magic + /// constants and later `/child` concatenation. + #[test] + fn strips_windows_verbatim_drive_prefix() { + assert_eq!( + strip_windows_verbatim_prefix(r"\\?\C:\work\app.php"), + r"C:\work\app.php" + ); + } + + /// Verifies verbatim UNC paths retain their ordinary leading UNC pair. + #[test] + fn converts_windows_verbatim_unc_prefix() { + assert_eq!( + strip_windows_verbatim_prefix(r"\\?\UNC\server\share\app.php"), + r"\\server\share\app.php" + ); + } + + /// Verifies ordinary Unix and Windows display paths are unchanged. + #[test] + fn preserves_non_verbatim_paths() { + assert_eq!(strip_windows_verbatim_prefix("/tmp/app.php"), "/tmp/app.php"); + assert_eq!( + strip_windows_verbatim_prefix(r"C:\work\app.php"), + r"C:\work\app.php" + ); + } +} diff --git a/src/types/checker/builtin_spl_classes/filesystem.rs b/src/types/checker/builtin_spl_classes/filesystem.rs index b241589875..b1c603bc96 100644 --- a/src/types/checker/builtin_spl_classes/filesystem.rs +++ b/src/types/checker/builtin_spl_classes/filesystem.rs @@ -1592,17 +1592,32 @@ fn glob_iterator_construct_body() -> Vec { /// Builds RecursiveDirectoryIterator hasChildren(). fn recursive_directory_has_children_body() -> Vec { - return_body(binary_expr( + let follows_symlinks = flag_enabled_expr(filesystem_flags_expr(), FS_FOLLOW_SYMLINKS); + let direct_directory = binary_expr( + function_call("is_dir", vec![file_path_arg_expr()]), + BinOp::And, + binary_expr( + follows_symlinks.clone(), + BinOp::Or, + not_expr(function_call("is_link", vec![file_path_arg_expr()])), + ), + ); + let relative_link_target = path_join_expr( + function_call("dirname", vec![file_path_arg_expr()]), + function_call("readlink", vec![file_path_arg_expr()]), + ); + let followed_relative_directory = binary_expr( binary_expr( - function_call("is_dir", vec![file_path_arg_expr()]), + follows_symlinks, BinOp::And, - binary_expr( - flag_enabled_expr(filesystem_flags_expr(), FS_FOLLOW_SYMLINKS), - BinOp::Or, - not_expr(function_call("is_link", vec![file_path_arg_expr()])), - ), + function_call("is_link", vec![file_path_arg_expr()]), ), BinOp::And, + function_call("is_dir", vec![relative_link_target]), + ); + return_body(binary_expr( + binary_expr(direct_directory, BinOp::Or, followed_relative_directory), + BinOp::And, not_expr(directory_is_dot_expr()), )) } diff --git a/src/types/checker/builtins/callables.rs b/src/types/checker/builtins/callables.rs index c1905ce90f..d2fe12dc6b 100644 --- a/src/types/checker/builtins/callables.rs +++ b/src/types/checker/builtins/callables.rs @@ -20,7 +20,7 @@ use crate::parser::ast::{CallableTarget, Expr, ExprKind, StaticReceiver}; use crate::types::array_constants::ARRAY_INT_CONSTANTS; use crate::types::{FunctionSig, PhpType, TypeEnv}; -use super::canonical_builtin_function_name; +use super::{canonical_builtin_function_name, canonical_builtin_function_name_on_platform}; use super::super::Checker; mod preg_replace_callback; @@ -973,7 +973,9 @@ pub(crate) fn check_call_user_func_array( return Ok(sig.return_type); } } - if let Some(builtin_name) = canonical_builtin_function_name(cb_name) { + if let Some(builtin_name) = + canonical_builtin_function_name_on_platform(cb_name, checker.target_platform) + { if let ExprKind::ArrayLiteral(elems) = &args[1].kind { if let Some(ret_ty) = checker.check_builtin(&builtin_name, elems, span, env)? diff --git a/src/types/checker/builtins/catalog.rs b/src/types/checker/builtins/catalog.rs index 50073ebe4b..fbd9b492dd 100644 --- a/src/types/checker/builtins/catalog.rs +++ b/src/types/checker/builtins/catalog.rs @@ -57,6 +57,19 @@ pub(crate) fn strict_php_hidden_builtin(canonical: &str) -> bool { .unwrap_or(false) } +/// Returns whether a PHP builtin is available on the selected target platform. +/// +/// PHP only registers `lchown()` and `lchgrp()` when `HAVE_LCHOWN` is defined; +/// its Windows builds do not define that capability, so those names must fall +/// through to ordinary undefined-function handling there. +pub(crate) fn builtin_available_on_platform( + canonical: &str, + platform: crate::codegen::platform::Platform, +) -> bool { + platform != crate::codegen::platform::Platform::Windows + || !matches!(canonical, "lchown" | "lchgrp") +} + /// Returns PHP-visible registry names plus compiler-resident call-like names, /// without applying the strict-PHP filter. /// @@ -94,6 +107,20 @@ pub(crate) fn supported_builtin_function_names() -> Vec<&'static str> { .collect() } +/// Returns the PHP-visible builtin names available on the selected platform. +/// +/// Metadata exporters intentionally use the platform-neutral catalog, while +/// runtime callable tables and target-specific compilation surfaces must use +/// this filtered view so conditionally registered PHP builtins stay absent. +pub(crate) fn supported_builtin_function_names_on_platform( + platform: crate::codegen::platform::Platform, +) -> Vec<&'static str> { + supported_builtin_function_names() + .into_iter() + .filter(|name| builtin_available_on_platform(&name.to_ascii_lowercase(), platform)) + .collect() +} + /// Converts a function name to lowercase and returns it if it is a supported builtin. /// /// Returns `None` if the name is neither registry-backed nor compiler-resident, @@ -112,6 +139,15 @@ pub(crate) fn canonical_builtin_function_name(name: &str) -> Option { } } +/// Canonicalizes a builtin name only when it is available on the selected platform. +pub(crate) fn canonical_builtin_function_name_on_platform( + name: &str, + platform: crate::codegen::platform::Platform, +) -> Option { + let canonical = canonical_builtin_function_name(name)?; + builtin_available_on_platform(&canonical, platform).then_some(canonical) +} + /// Returns true only for PHP-visible builtin functions (non-internal builtins). /// /// Checks both compiler-resident names and the builtin registry. Registry entries @@ -179,6 +215,49 @@ mod tests { ); } + /// Verifies the Windows builtin surface omits link ownership functions, + /// while Unix targets retain both names. + #[test] + fn platform_availability_matches_php_lchown_capability() { + use crate::codegen::platform::Platform; + + for name in ["lchown", "lchgrp"] { + assert!(!builtin_available_on_platform(name, Platform::Windows)); + assert!(builtin_available_on_platform(name, Platform::Linux)); + assert!(builtin_available_on_platform(name, Platform::MacOS)); + } + assert!(builtin_available_on_platform("chown", Platform::Windows)); + assert!(builtin_available_on_platform("chgrp", Platform::Windows)); + } + + /// Verifies target-aware catalog lookup removes link ownership functions + /// only on Windows while preserving their Unix callable metadata. + #[test] + fn target_catalog_lookup_filters_lchown_capability() { + use crate::codegen::platform::Platform; + + for name in ["lchown", "lchgrp"] { + assert!( + canonical_builtin_function_name_on_platform(name, Platform::Windows).is_none() + ); + assert_eq!( + canonical_builtin_function_name_on_platform(name, Platform::Linux).as_deref(), + Some(name) + ); + assert_eq!( + canonical_builtin_function_name_on_platform(name, Platform::MacOS).as_deref(), + Some(name) + ); + } + + let windows = supported_builtin_function_names_on_platform(Platform::Windows); + assert!(!windows.contains(&"lchown")); + assert!(!windows.contains(&"lchgrp")); + let linux = supported_builtin_function_names_on_platform(Platform::Linux); + assert!(linux.contains(&"lchown")); + assert!(linux.contains(&"lchgrp")); + } + /// Verifies strict mode hides extension builtins from every catalog surface: /// canonical lookup, PHP-visibility, the supported-name set, and the /// `buffer_new` catalog-name-only entry. Strict state is thread-local (and diff --git a/src/types/checker/builtins/mod.rs b/src/types/checker/builtins/mod.rs index d864c6f49e..f5b8bee25e 100644 --- a/src/types/checker/builtins/mod.rs +++ b/src/types/checker/builtins/mod.rs @@ -22,9 +22,10 @@ use crate::types::{PhpType, TypeEnv}; use super::Checker; pub(crate) use catalog::{ - canonical_builtin_function_name, is_php_visible_builtin_function, + builtin_available_on_platform, canonical_builtin_function_name, + canonical_builtin_function_name_on_platform, is_php_visible_builtin_function, is_supported_builtin_function, strict_php_hidden_builtin, - supported_builtin_function_names, + supported_builtin_function_names_on_platform, }; pub(crate) use callables::{ array_element_type, array_filter_callback_arg_types, callback_supports_complex_descriptor_env, @@ -54,6 +55,21 @@ impl Checker { } } + /// Records that a Windows target requires the given shared library. + /// + /// No-op on non-Windows targets. Used for libraries that live in libc on + /// Linux (glibc/musl) and libSystem on macOS but need explicit linkage on + /// Windows because msvcrt does not ship them — e.g. `iconv`, which the + /// `convert.iconv.*` stream filter lowers to `iconv_open`/`iconv`/ + /// `iconv_close` C symbols resolved by a cross-built libiconv in CI. + pub(crate) fn require_windows_builtin_library(&mut self, library: &str) { + if self.target_platform == crate::codegen::platform::Platform::Windows + && !self.required_libraries.iter().any(|lib| lib == library) + { + self.required_libraries.push(library.to_string()); + } + } + /// Type-checks a PHP builtin function call, returning the inferred return type or `None` if unhandled. pub fn check_builtin( &mut self, @@ -67,6 +83,9 @@ impl Checker { // eagerly inferred by argument normalization. Their handlers inspect the // raw operands directly. let builtin_key = crate::names::php_symbol_key(name.trim_start_matches('\\')); + if !catalog::builtin_available_on_platform(&builtin_key, self.target_platform) { + return Ok(None); + } // `--strict-php` hides extension builtins entirely: the call must fall // through to user-function resolution and the standard undefined-function // diagnostics, mirroring PHP where these names do not exist. This must @@ -131,6 +150,9 @@ impl Checker { crate::builtins::semantics::BuiltinRequirement::MacOsLibrary(library) => { self.require_macos_builtin_library(library); } + crate::builtins::semantics::BuiltinRequirement::WindowsLibrary(library) => { + self.require_windows_builtin_library(library); + } crate::builtins::semantics::BuiltinRequirement::RuntimeFeature(_) => {} } } diff --git a/src/types/checker/builtins/spl.rs b/src/types/checker/builtins/spl.rs index f663768821..93f340cadc 100644 --- a/src/types/checker/builtins/spl.rs +++ b/src/types/checker/builtins/spl.rs @@ -270,7 +270,10 @@ pub(crate) fn check_iterator_apply_dynamic_callback( return Ok(()); } } - if let Some(builtin_name) = super::canonical_builtin_function_name(cb_name) { + if let Some(builtin_name) = super::canonical_builtin_function_name_on_platform( + cb_name, + checker.target_platform, + ) { if let Some(sig) = crate::types::first_class_callable_builtin_sig(&builtin_name) { reject_dynamic_ref_args(&sig, span)?; return Ok(()); diff --git a/src/types/checker/callables/first_class.rs b/src/types/checker/callables/first_class.rs index ef7cb828d9..c1d79a3680 100644 --- a/src/types/checker/callables/first_class.rs +++ b/src/types/checker/callables/first_class.rs @@ -65,7 +65,12 @@ impl Checker { deprecation: None, }); } - if crate::name_resolver::is_builtin_function(function_name) { + if crate::name_resolver::is_builtin_function(function_name) + && crate::types::checker::builtins::builtin_available_on_platform( + &crate::names::php_symbol_key(function_name.trim_start_matches('\\')), + self.target_platform, + ) + { return crate::types::first_class_callable_builtin_sig(function_name) .ok_or_else(|| { CompileError::new( @@ -242,7 +247,10 @@ impl Checker { } match target { CallableTarget::Function(name) => { - if crate::name_resolver::is_builtin_function(name.as_str()) { + if crate::name_resolver::is_builtin_function_on_platform( + name.as_str(), + self.target_platform, + ) { return Ok(base_sig); } let normalized_args = diff --git a/src/types/checker/driver/functions.rs b/src/types/checker/driver/functions.rs index a03bdc6e99..2d11b5435c 100644 --- a/src/types/checker/driver/functions.rs +++ b/src/types/checker/driver/functions.rs @@ -13,6 +13,7 @@ use std::collections::{HashMap, HashSet}; use crate::errors::CompileError; use crate::names::php_symbol_key; use crate::parser::ast::{Expr, Program, StmtKind, TypeExpr}; +use crate::types::checker::builtins::canonical_builtin_function_name_on_platform; use crate::types::FunctionSig; use super::super::{Checker, FnDecl}; @@ -38,7 +39,7 @@ impl Checker { continue; } if let Some(builtin) = - crate::types::checker::builtins::canonical_builtin_function_name(name) + canonical_builtin_function_name_on_platform(name, self.target_platform) { errors.push(CompileError::new( stmt.span, @@ -71,7 +72,7 @@ impl Checker { continue; } if let Some(builtin) = - crate::types::checker::builtins::canonical_builtin_function_name(name) + canonical_builtin_function_name_on_platform(name, self.target_platform) { errors.push(CompileError::new( stmt.span, diff --git a/src/types/checker/driver/init.rs b/src/types/checker/driver/init.rs index 00d136b5ef..caf8412a88 100644 --- a/src/types/checker/driver/init.rs +++ b/src/types/checker/driver/init.rs @@ -26,11 +26,13 @@ use super::super::Checker; impl Checker { /// Constructs a new `Checker` with pre-populated builtin constants and empty declaration tables. /// - /// Initializes the global constant map with PHP built-in constants (`PHP_OS`, `SID`, pathinfo - /// constants, `ENT_*` HTML-escaping flags, `FNM_*` flags, stream resources, and lock flags), - /// array, JSON, stream, date, and preg constants, `PHP_SESSION_*` - /// session-status constants, and `E_*` error-level constants. All other tables (function declarations, - /// classes, interfaces, enums, etc.) are initialized empty. + /// Initializes the global constant map with PHP built-in constants (`PHP_OS`, + /// `PHP_OS_FAMILY`, `SID`, `PHP_INT_SIZE`, the emulated-PHP version constants + /// (`PHP_VERSION`, `PHP_MAJOR_VERSION`, `PHP_MINOR_VERSION`, `PHP_RELEASE_VERSION`, + /// `PHP_VERSION_ID`, `PHP_EXTRA_VERSION`), pathinfo + /// constants, `ENT_*` HTML-escaping flags, `FNM_*` flags, `STDIN`/`STDOUT`/`STDERR` stream + /// resources, `LOCK_*` constants), array, JSON, stream, date, preg, session, and error-level + /// constants. All other declaration tables are initialized empty. /// /// # Arguments /// * `target_platform` - The compilation target platform, stored for use in platform-specific @@ -44,6 +46,17 @@ impl Checker { // Deprecated session-id constant; elephc is cookie-only so it always // resolves to the empty string (see `codegen::prescan::collect_constants`). constants.insert("SID".to_string(), PhpType::Str); + constants.insert("PHP_EOL".to_string(), PhpType::Str); + constants.insert("DIRECTORY_SEPARATOR".to_string(), PhpType::Str); + constants.insert("PATH_SEPARATOR".to_string(), PhpType::Str); + constants.insert("PHP_OS_FAMILY".to_string(), PhpType::Str); + constants.insert("PHP_INT_SIZE".to_string(), PhpType::Int); + constants.insert("PHP_VERSION".to_string(), PhpType::Str); + constants.insert("PHP_MAJOR_VERSION".to_string(), PhpType::Int); + constants.insert("PHP_MINOR_VERSION".to_string(), PhpType::Int); + constants.insert("PHP_RELEASE_VERSION".to_string(), PhpType::Int); + constants.insert("PHP_VERSION_ID".to_string(), PhpType::Int); + constants.insert("PHP_EXTRA_VERSION".to_string(), PhpType::Str); constants.insert("PATHINFO_DIRNAME".to_string(), PhpType::Int); constants.insert("PATHINFO_BASENAME".to_string(), PhpType::Int); constants.insert("PATHINFO_EXTENSION".to_string(), PhpType::Int); @@ -100,9 +113,6 @@ impl Checker { constants.insert("M_PI_4".to_string(), PhpType::Float); constants.insert("M_LOG2E".to_string(), PhpType::Float); constants.insert("M_LOG10E".to_string(), PhpType::Float); - constants.insert("PHP_EOL".to_string(), PhpType::Str); - constants.insert("DIRECTORY_SEPARATOR".to_string(), PhpType::Str); - Self { target_platform, fn_decls: HashMap::new(), diff --git a/src/types/checker/inference/expr/effects.rs b/src/types/checker/inference/expr/effects.rs index 349f99f132..3717d0876a 100644 --- a/src/types/checker/inference/expr/effects.rs +++ b/src/types/checker/inference/expr/effects.rs @@ -256,6 +256,19 @@ impl Checker { } } } + if builtin_name.eq_ignore_ascii_case("proc_open") { + if let Some(arg) = expanded_args.get(2) { + if let Some(name) = proc_open_pipes_output_var(arg) { + env.insert( + name.clone(), + PhpType::AssocArray { + key: Box::new(PhpType::Int), + value: Box::new(PhpType::stream_resource()), + }, + ); + } + } + } if builtin_name.eq_ignore_ascii_case("unset") { for arg in &expanded_args { promote_indexed_local_for_element_unset(arg, env); @@ -456,6 +469,20 @@ fn preg_match_output_var(arg: &Expr) -> Option<&String> { } } +/// Returns the plain local populated by `proc_open`'s `$pipes` output parameter. +/// +/// PHP writes an integer-keyed map of stream resources through this by-reference +/// parameter. The checker records that post-call fact only for a local variable: +/// writing through an array element or property would require alias-aware flow +/// tracking, which this targeted output-parameter rule deliberately does not add. +fn proc_open_pipes_output_var(arg: &Expr) -> Option<&String> { + match &arg.kind { + ExprKind::Variable(name) => Some(name), + ExprKind::NamedArg { value, .. } => proc_open_pipes_output_var(value), + _ => None, + } +} + /// Promotes a packed indexed-array local to an associative array when one of its elements is /// removed via `unset($arr[$key])`. /// diff --git a/src/types/checker/inference/objects/constructors.rs b/src/types/checker/inference/objects/constructors.rs index d8b93f32da..0a5fb829ac 100644 --- a/src/types/checker/inference/objects/constructors.rs +++ b/src/types/checker/inference/objects/constructors.rs @@ -77,6 +77,7 @@ impl Checker { if is_phar_archive_class(&class_name) { self.require_phar_archive_libraries(); } + self.require_datetime_tz_bridge_if_needed(&class_name); if matches!( class_name.as_str(), "CallbackFilterIterator" | "RecursiveCallbackFilterIterator" @@ -205,11 +206,39 @@ impl Checker { Ok(()) } - /// Records the PHAR bridge and decompression libraries needed by PHAR archive helpers. + /// Records the pure-Rust PHAR bridge needed by archive helpers. + /// + /// Gzip and bzip2 decoding live inside `elephc-phar`; PHAR objects must not + /// pull the unrelated system `libz`/`libbz2` stream-filter dependencies. pub(crate) fn require_phar_archive_libraries(&mut self) { self.require_builtin_library("elephc_phar"); - self.require_builtin_library("z"); - self.require_builtin_library("bz2"); + } + + /// Records the windows-only elephc-tz bridge requirement for the OOP + /// datetime surface (`DateTime`/`DateTimeZone`/`DateTimeImmutable`/ + /// `DatePeriod`; see `is_elephc_tz_backed_datetime_class`). + /// + /// The synthetic methods these classes inject (`getOffset`, `format`, + /// `setTimezone`, the zone-aware constructor, `getTimestamp`, + /// `createFromFormat`, `DatePeriod::createFromISO8601String`, ...) resolve + /// IANA zone offsets through the same elephc-tz bridge as the procedural + /// `date`/`mktime`/`strtotime` builtins (see + /// `crate::builtins::system::date::check`), directly or by constructing a + /// `DateTime`/`DateTimeImmutable` internally. Unlike those procedural + /// builtins, the OOP methods' PHP bodies are synthetic and never flow + /// through `type_check_methods_until_stable` (they are not part of the + /// user program's `flattened_classes`), so their internal `date()`/ + /// `mktime()`/`new DateTime(...)` calls never reach `check_builtin` or + /// `infer_new_object_type` and never trigger the requirement themselves. + /// This call site — reached whenever a program actually constructs or + /// statically calls one of the tz-backed classes — is the pay-for-use + /// substitute: `require_windows_builtin_library` is a no-op off windows, + /// and a program that never touches these classes never links + /// `-lelephc_tz`. + pub(crate) fn require_datetime_tz_bridge_if_needed(&mut self, class_name: &str) { + if is_elephc_tz_backed_datetime_class(class_name) { + self.require_windows_builtin_library("elephc_tz"); + } } /// Returns true when construct internal iterator from builtin get iterator. @@ -596,8 +625,15 @@ impl Checker { ) -> Result, CompileError> { let lookup_name = function_name.trim_start_matches('\\'); let builtin_key = php_symbol_key(lookup_name); - if let Some(sig) = crate::types::first_class_callable_builtin_sig(&builtin_key) { - return Ok(Some(sig)); + if crate::types::checker::builtins::canonical_builtin_function_name_on_platform( + &builtin_key, + self.target_platform, + ) + .is_some() + { + if let Some(sig) = crate::types::first_class_callable_builtin_sig(&builtin_key) { + return Ok(Some(sig)); + } } let canonical = match self.canonical_function_name_folded(lookup_name) { @@ -1136,7 +1172,7 @@ impl Checker { name: &str, span: crate::span::Span, ) -> Result<(), CompileError> { - if crate::name_resolver::is_builtin_function(name) { + if crate::name_resolver::is_builtin_function_on_platform(name, self.target_platform) { return Ok(()); } @@ -1240,6 +1276,35 @@ fn is_phar_archive_class(class_name: &str) -> bool { matches!(class_name, "Phar" | "PharData") } +/// Returns `true` if `class_name` is one of the builtin OOP datetime classes +/// (`DateTime`, `DateTimeImmutable`, `DateTimeZone`, `DatePeriod`) whose +/// synthetic methods resolve IANA zone offsets through the windows-only +/// elephc-tz bridge, directly or transitively. +/// +/// `DatePeriod` is included even though it models a range rather than a +/// single instant: its `createFromISO8601String` static factory (the only +/// entry point that accepts a raw ISO 8601 string rather than pre-built +/// `DateTimeInterface` objects — see `date_period.rs`'s module preamble) +/// constructs a `new DateTime(...)` in its own synthetic body, which never +/// flows through `infer_new_object_type` for the same reason described on +/// `require_datetime_tz_bridge_if_needed`. The `(start, interval, end)`/ +/// `(start, interval, recurrences)` constructor forms take +/// already-constructed `DateTimeInterface` objects, so those paths ride free +/// on the requirement the caller's own `new DateTime(...)`/`new +/// DateTimeImmutable(...)` already recorded. +/// +/// `DateInterval` is deliberately excluded: it models a pure duration, and +/// neither its constructor (parses `PnYnMnD...` strings) nor +/// `createFromDateString` (parses relative-unit strings like `"3 days"`) nor +/// `format` ever call `date()`/`mktime()`/`strtotime()` or construct a +/// `DateTime`/`DateTimeZone`, so it never resolves a zone offset. +fn is_elephc_tz_backed_datetime_class(class_name: &str) -> bool { + matches!( + class_name, + "DateTime" | "DateTimeImmutable" | "DateTimeZone" | "DatePeriod" + ) +} + /// Returns `true` if the attribute name/arg slices are mismatched or any /// arg is `None` (indicating unsupported metadata). fn attributes_have_unsupported_args( diff --git a/src/types/checker/inference/objects/constructors/reflection.rs b/src/types/checker/inference/objects/constructors/reflection.rs index 2ee5ffccc8..8aa6fe280a 100644 --- a/src/types/checker/inference/objects/constructors/reflection.rs +++ b/src/types/checker/inference/objects/constructors/reflection.rs @@ -33,7 +33,13 @@ impl Checker { self.canonical_function_name_folded(function_name.trim_start_matches('\\')) else { let builtin_key = php_symbol_key(function_name.trim_start_matches('\\')); - if crate::types::first_class_callable_builtin_sig(&builtin_key).is_some() { + if crate::types::checker::builtins::canonical_builtin_function_name_on_platform( + &builtin_key, + self.target_platform, + ) + .is_some() + && crate::types::first_class_callable_builtin_sig(&builtin_key).is_some() + { return Ok(()); } return Err(CompileError::new( diff --git a/src/types/checker/inference/objects/methods.rs b/src/types/checker/inference/objects/methods.rs index d295b5e401..40b21be815 100644 --- a/src/types/checker/inference/objects/methods.rs +++ b/src/types/checker/inference/objects/methods.rs @@ -788,6 +788,15 @@ impl Checker { } }; let class_name = resolved_class_name.as_str(); + // Static factories (`DateTime::createFromFormat`, `DateTimeImmutable:: + // createFromTimestamp`, `DatePeriod::createFromISO8601String`, ...) + // construct one of the OOP datetime classes without ever going through + // `infer_new_object_type`'s `new` path, so the windows-only elephc-tz + // bridge requirement is recorded here too (see + // `require_datetime_tz_bridge_if_needed` for why this call-site gate + // exists instead of the synthetic method bodies triggering it + // themselves). + self.require_datetime_tz_bridge_if_needed(class_name); // `Closure::bind($closure, $newThis [, $scope])` is the static form of // `$closure->bindTo(...)`: it returns a new closure with `$this` rebound. // `$scope` is accepted and ignored (closed-world visibility). diff --git a/src/types/result.rs b/src/types/result.rs index 2e611b0a1e..eb7be5cf6d 100644 --- a/src/types/result.rs +++ b/src/types/result.rs @@ -133,6 +133,109 @@ mod tests { assert_eq!(mac.required_libraries, vec!["elephc_crypto"]); } + /// Verifies Windows rejects link-ownership builtins that php-src omits + /// when `HAVE_LCHOWN` is unavailable. + #[test] + fn windows_rejects_unavailable_lchown_builtins() { + let target = Target::new(Platform::Windows, Arch::X86_64); + for name in ["lchown", "lchgrp"] { + let program = parse_program(&format!("index; } + public function key(): mixed { return $this->index; } + public function next(): void { $this->index = $this->index + 1; } + public function rewind(): void { $this->index = 0; } + public function valid(): bool { return $this->index < 1; } +} +iterator_apply( + new WindowsLinkOwnerIterator(), + "lchgrp", + ["link.txt", -1], +); +"#, + "Undefined function: lchgrp", + ), + ] { + let program = parse_program(source); + let error = check_with_target(&program, target) + .expect_err("Windows callable surface must omit lchown-family builtins"); + assert!( + error.message.contains(expected), + "unexpected Windows callable diagnostic: {}", + error.message + ); + } + } + + /// Verifies Windows may declare user functions whose names are only + /// reserved by php-src on platforms that provide `HAVE_LCHOWN`. + #[test] + fn windows_allows_user_lchown_function_declarations() { + let target = Target::new(Platform::Windows, Arch::X86_64); + for name in ["lchown", "lchgrp"] { + let program = parse_program(&format!( + " 0; } +$callback = lchgrp(...); +$filter = new CallbackFilterIterator(new ArrayIterator([1]), $callback); +foreach ($filter as $value) { echo $value; } +"#, + ] { + let program = parse_program(source); + check_with_target(&program, target) + .expect("Windows must specialize user functions named after unavailable builtins"); + } + } + /// Verifies enum class metadata preserves flattened trait relation data for runtime reflection. #[test] fn test_enum_class_info_preserves_trait_metadata() { diff --git a/src/windows_toolchain.rs b/src/windows_toolchain.rs new file mode 100644 index 0000000000..93de1c7b33 --- /dev/null +++ b/src/windows_toolchain.rs @@ -0,0 +1,327 @@ +//! Purpose: +//! Selects the assembler and linker driver for Windows x86-64 PE output. +//! Preserves the MinGW GNU default while providing an opt-in LLVM path. +//! +//! Called from: +//! - `crate::linker` for user objects and final image linking. +//! - `crate::runtime_cache` for cached runtime-object assembly. +//! +//! Key details: +//! - LLVM mode retains the `x86_64-pc-windows-gnu` ABI used by bridge archives. +//! - Toolchain selection never advertises Guard CF by itself. + +use std::env; +use std::hash::{Hash, Hasher}; +use std::path::{Path, PathBuf}; +use std::process::Command; + +/// Environment variable selecting the Windows assembler/linker family. +const WINDOWS_TOOLCHAIN_ENV: &str = "ELEPHC_WINDOWS_TOOLCHAIN"; + +/// Supported Windows PE toolchain families. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum WindowsToolchain { + /// MinGW binutils assembler plus the MinGW GCC linker driver. + Gnu, + /// Clang integrated assembler plus LLD through the Clang driver. + Llvm, +} + +impl WindowsToolchain { + /// Reads the Windows toolchain selection, defaulting to the established GNU path. + pub(crate) fn configured() -> Result { + Self::parse(env::var(WINDOWS_TOOLCHAIN_ENV).ok().as_deref()) + } + + /// Parses a toolchain selector without reading process-global environment state. + fn parse(value: Option<&str>) -> Result { + match value.map(str::trim).filter(|value| !value.is_empty()) { + None | Some("gnu") | Some("mingw") => Ok(Self::Gnu), + Some("llvm") | Some("gnullvm") => Ok(Self::Llvm), + Some(value) => Err(format!( + "invalid {WINDOWS_TOOLCHAIN_ENV} value '{value}'; expected 'gnu' or 'llvm'" + )), + } + } + + /// Returns the stable cache-key component for objects produced by this toolchain. + pub(crate) fn cache_key(self) -> &'static str { + match self { + Self::Gnu => "gnu", + Self::Llvm => "llvm", + } + } +} + +/// Constructs the configured Windows x86-64 assembler command. +/// +/// LLVM mode invokes Clang with an explicit GNU Windows target, which selects +/// the integrated assembler and emits the same PE/COFF object format consumed +/// by the existing MinGW ABI runtime and bridge archives. +pub(crate) fn assembler_command(asm_path: &Path, obj_path: &Path) -> Result { + match WindowsToolchain::configured()? { + WindowsToolchain::Gnu => { + let mut command = Command::new("x86_64-w64-mingw32-as"); + command.arg("-o").arg(obj_path).arg(asm_path); + Ok(command) + } + WindowsToolchain::Llvm => { + let clang = env::var_os("ELEPHC_WINDOWS_CLANG").unwrap_or_else(|| "clang".into()); + let mut command = Command::new(clang); + command + .arg("--target=x86_64-w64-windows-gnu") + .arg("-c") + .arg("-o") + .arg(obj_path) + .arg(asm_path); + Ok(command) + } + } +} + +/// Constructs the configured Windows x86-64 linker driver command. +/// +/// LLVM mode still targets the GNU Windows ABI so MinGW import libraries and +/// Rust `x86_64-pc-windows-gnu` bridge archives remain link-compatible. The +/// MinGW sysroot is resolved explicitly because cross-target Clang installations +/// cannot reliably infer Homebrew, distro, or gnullvm layouts. +pub(crate) fn linker_command() -> Result { + match WindowsToolchain::configured()? { + WindowsToolchain::Gnu => Ok(Command::new("x86_64-w64-mingw32-gcc")), + WindowsToolchain::Llvm => { + let clang = env::var_os("ELEPHC_WINDOWS_CLANG").unwrap_or_else(|| "clang".into()); + let lld = env::var("ELEPHC_WINDOWS_LLD").unwrap_or_else(|_| "lld".to_string()); + let lld = lld_for_clang(&lld)?; + let sysroot = llvm_sysroot()?; + let gcc_support_dir = llvm_gcc_support_library_dir()?; + let mut command = Command::new(clang); + command + .arg("--target=x86_64-w64-windows-gnu") + .arg(format!("--sysroot={}", sysroot.display())) + // Debian's MinGW GCC support libraries live outside the sysroot + // (`/usr/lib/gcc///`). Clang otherwise passes + // `-lgcc -lgcc_eh` to LLD without a search path for either. + .arg(format!("-L{}", gcc_support_dir.display())) + .arg(format!("-fuse-ld={}", lld.display())); + Ok(command) + } + } +} + +/// Gives Clang's linker selection logic an `ld.lld`-named path for rustup's +/// generic `rust-lld` driver, whose flavor is otherwise ambiguous at startup. +fn lld_for_clang(configured: &str) -> Result { + let path = PathBuf::from(configured); + let basename = path.file_name().and_then(|name| name.to_str()).unwrap_or_default(); + if basename != "rust-lld" && basename != "rust-lld.exe" { + return Ok(path); + } + if !path.is_file() { + return Err(format!( + "ELEPHC_WINDOWS_LLD='{}' does not name a file", + path.display() + )); + } + + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + path.hash(&mut hasher); + let metadata = std::fs::metadata(&path).map_err(|error| { + format!( + "failed to inspect ELEPHC_WINDOWS_LLD '{}': {error}", + path.display() + ) + })?; + metadata.len().hash(&mut hasher); + metadata.modified().ok().hash(&mut hasher); + let alias_dir = env::temp_dir().join(format!("elephc-rust-lld-driver-{:016x}", hasher.finish())); + std::fs::create_dir_all(&alias_dir).map_err(|error| { + format!( + "failed to create rust-lld driver directory '{}': {error}", + alias_dir.display() + ) + })?; + let alias_name = if cfg!(windows) { "ld.lld.exe" } else { "ld.lld" }; + let alias = alias_dir.join(alias_name); + if !alias.exists() { + create_lld_alias(&path, &alias)?; + } + Ok(alias) +} + +/// Creates the flavor-selecting `ld.lld` alias used by Clang. +#[cfg(unix)] +fn create_lld_alias(source: &Path, alias: &Path) -> Result<(), String> { + match std::os::unix::fs::symlink(source, alias) { + Ok(()) => Ok(()), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => Ok(()), + Err(error) => Err(format!( + "failed to alias rust-lld '{}' as '{}': {error}", + source.display(), + alias.display() + )), + } +} + +/// Creates the flavor-selecting `ld.lld.exe` alias used by Clang. +#[cfg(not(unix))] +fn create_lld_alias(source: &Path, alias: &Path) -> Result<(), String> { + match std::fs::hard_link(source, alias) { + Ok(()) => Ok(()), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => Ok(()), + Err(error) => Err(format!( + "failed to alias rust-lld '{}' as '{}': {error}", + source.display(), + alias.display() + )), + } +} + +/// Resolves the MinGW sysroot used by the LLVM Windows linker driver. +fn llvm_sysroot() -> Result { + if let Some(path) = env::var_os("ELEPHC_WINDOWS_SYSROOT") { + let path = PathBuf::from(path); + if path.is_dir() { + return Ok(path); + } + return Err(format!( + "ELEPHC_WINDOWS_SYSROOT='{}' is not a directory", + path.display() + )); + } + + let gcc = env::var_os("ELEPHC_WINDOWS_GCC").unwrap_or_else(|| "x86_64-w64-mingw32-gcc".into()); + let output = Command::new(&gcc) + .arg("-print-sysroot") + .output() + .map_err(|error| { + format!( + "LLVM Windows toolchain needs a MinGW sysroot; failed to run '{} -print-sysroot': {error}. Set ELEPHC_WINDOWS_SYSROOT explicitly", + Path::new(&gcc).display() + ) + })?; + let value = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let path = PathBuf::from(&value); + if output.status.success() && !value.is_empty() && path.is_dir() { + Ok(path) + } else { + Err(format!( + "LLVM Windows toolchain could not resolve a MinGW sysroot from '{} -print-sysroot'; set ELEPHC_WINDOWS_SYSROOT explicitly", + Path::new(&gcc).display() + )) + } +} + +/// Resolves the GCC private library directory required by Clang's GNU Windows link. +/// +/// MinGW packages on Debian place `libgcc.a` and `libgcc_eh.a` under GCC's +/// versioned directory rather than inside the target sysroot. Querying the +/// selected GCC driver keeps Homebrew, distro, and custom toolchains aligned. +fn llvm_gcc_support_library_dir() -> Result { + let gcc = env::var_os("ELEPHC_WINDOWS_GCC").unwrap_or_else(|| "x86_64-w64-mingw32-gcc".into()); + let output = Command::new(&gcc) + .arg("-print-libgcc-file-name") + .output() + .map_err(|error| { + format!( + "LLVM Windows toolchain needs MinGW GCC support libraries; failed to run '{} -print-libgcc-file-name': {error}", + Path::new(&gcc).display() + ) + })?; + let value = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let libgcc = PathBuf::from(&value); + if output.status.success() && !value.is_empty() && libgcc.is_file() { + return gcc_support_library_dir_from(&libgcc); + } + Err(format!( + "LLVM Windows toolchain could not resolve GCC support library from '{} -print-libgcc-file-name'; set ELEPHC_WINDOWS_GCC to a MinGW GCC driver", + Path::new(&gcc).display() + )) +} + +/// Returns the linker-search directory containing a verified GCC support archive. +fn gcc_support_library_dir_from(libgcc: &Path) -> Result { + libgcc.parent().map(Path::to_path_buf).ok_or_else(|| { + format!( + "LLVM Windows toolchain could not determine the parent directory of GCC support library '{}'", + libgcc.display() + ) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Verifies the absent selector preserves the established MinGW GNU default. + #[test] + fn defaults_to_gnu() { + assert_eq!(WindowsToolchain::parse(None), Ok(WindowsToolchain::Gnu)); + assert_eq!(WindowsToolchain::parse(Some("")), Ok(WindowsToolchain::Gnu)); + } + + /// Verifies documented GNU, LLVM, and gnullvm selector spellings. + #[test] + fn accepts_documented_values() { + assert_eq!(WindowsToolchain::parse(Some("gnu")), Ok(WindowsToolchain::Gnu)); + assert_eq!(WindowsToolchain::parse(Some("mingw")), Ok(WindowsToolchain::Gnu)); + assert_eq!(WindowsToolchain::parse(Some("llvm")), Ok(WindowsToolchain::Llvm)); + assert_eq!(WindowsToolchain::parse(Some("gnullvm")), Ok(WindowsToolchain::Llvm)); + } + + /// Verifies a typo produces an actionable diagnostic instead of silently changing tools. + #[test] + fn rejects_unknown_values() { + let error = WindowsToolchain::parse(Some("msvc")).expect_err("MSVC ABI is unsupported"); + assert!(error.contains(WINDOWS_TOOLCHAIN_ENV)); + assert!(error.contains("expected 'gnu' or 'llvm'")); + } + + /// Verifies a normal LLD selector is passed through without filesystem changes. + #[test] + fn preserves_named_lld_driver() { + assert_eq!(lld_for_clang("lld").expect("named driver"), PathBuf::from("lld")); + assert_eq!( + lld_for_clang("/opt/toolchain/ld.lld").expect("absolute driver"), + PathBuf::from("/opt/toolchain/ld.lld") + ); + } + + /// Verifies rustup's generic driver receives an `ld.lld` alias so Clang selects GNU flavor. + #[test] + fn aliases_rust_lld_for_clang_flavor_detection() { + let fixture_dir = env::temp_dir().join(format!("elephc-rust-lld-test-{}", std::process::id())); + std::fs::create_dir_all(&fixture_dir).expect("create rust-lld fixture directory"); + let fixture = fixture_dir.join(if cfg!(windows) { "rust-lld.exe" } else { "rust-lld" }); + std::fs::write(&fixture, b"fixture").expect("write rust-lld fixture"); + + let alias = lld_for_clang(fixture.to_str().expect("UTF-8 fixture path")) + .expect("create flavor-selecting alias"); + + assert_eq!( + alias.file_name().and_then(|name| name.to_str()), + Some(if cfg!(windows) { "ld.lld.exe" } else { "ld.lld" }) + ); + assert_eq!(std::fs::read(&alias).expect("read alias"), b"fixture"); + let _ = std::fs::remove_dir_all(&fixture_dir); + if let Some(alias_dir) = alias.parent() { + let _ = std::fs::remove_dir_all(alias_dir); + } + } + + /// Verifies a resolved GCC support archive yields its containing linker directory. + #[test] + fn derives_gcc_support_directory_from_libgcc_archive() { + let fixture_dir = env::temp_dir().join(format!("elephc-libgcc-test-{}", std::process::id())); + std::fs::create_dir_all(&fixture_dir).expect("create libgcc fixture directory"); + let archive = fixture_dir.join("libgcc.a"); + std::fs::write(&archive, b"fixture").expect("write libgcc fixture archive"); + + let parent = archive.parent().expect("fixture archive parent").to_path_buf(); + assert_eq!( + gcc_support_library_dir_from(&archive).expect("derive GCC support directory"), + parent + ); + + let _ = std::fs::remove_dir_all(&fixture_dir); + } +} diff --git a/tests/cdylib_tests.rs b/tests/cdylib_tests.rs index 1557e12b06..6e72de0084 100644 --- a/tests/cdylib_tests.rs +++ b/tests/cdylib_tests.rs @@ -211,6 +211,92 @@ fn test_cdylib_dynamic_symbols_expose_only_public_abi_on_linux() { fs::remove_dir_all(&dir).ok(); } +/// Verifies the Windows GNU cross path produces a PE DLL, a conventional +/// import library, undecorated exports, and an import library consumable by a +/// separately compiled C executable. Runtime execution remains a Wine/Windows +/// runner gate rather than a host-independent structural check. +#[test] +fn test_windows_cdylib_cross_links_a_c_consumer_when_mingw_is_available() { + let mingw = "x86_64-w64-mingw32-gcc"; + if Command::new(mingw).arg("--version").output().is_err() { + eprintln!("skipping Windows cdylib cross-link test: MinGW-w64 unavailable"); + return; + } + + let dir = make_test_dir("elephc_windows_cdylib"); + fs::write( + dir.join("native.php"), + r#" +__declspec(dllimport) double mixed(long long, double, long long, double, long long); +__declspec(dllimport) long long string_after_three(long long, long long, long long, const char *, size_t); +int main(void) { + return mixed(1, 2.0, 3, 4.0, 5) == 15.0 && + string_after_three(1, 2, 3, "abcd", 4) == 10 ? 0 : 1; +} +"#, + ) + .unwrap(); + let linked = Command::new(mingw) + .current_dir(&dir) + .args(["-o", "consumer.exe", "consumer.c", "-L.", "-lnative"]) + .output() + .expect("failed to spawn MinGW C compiler"); + assert!( + linked.status.success(), + "MinGW C consumer link failed:\n{}", + String::from_utf8_lossy(&linked.stderr) + ); + assert!(dir.join("consumer.exe").exists()); + + fs::remove_dir_all(&dir).ok(); +} + /// Verifies that `#[Export]` signatures outside the v1 scalar set are rejected /// with a compile error instead of producing a trampoline with an undefined /// C ABI (arrays have no defined marshaling in v1). diff --git a/tests/codegen/arrays/indexed/slice_stack_range.rs b/tests/codegen/arrays/indexed/slice_stack_range.rs index 4c137d932b..6e995be73d 100644 --- a/tests/codegen/arrays/indexed/slice_stack_range.rs +++ b/tests/codegen/arrays/indexed/slice_stack_range.rs @@ -159,3 +159,37 @@ echo implode(",", $removed), "|", implode(",", $a3); ); assert_eq!(out, "20,30|30,40,50|2,3|1,4,5"); } + +/// Verifies `array_slice()` and `array_splice()` read a negative length as php does. +#[test] +fn test_array_slice_and_splice_negative_length() { + // php reads a negative length as "stop that many elements before the end". + // elephc used -1 in-band to mean "no length argument", so an explicit -1 was + // indistinguishable from an omitted one, and any other negative value slipped + // past the upper clamp. For array_splice that value then became the capacity + // handed to __rt_array_new and the shift-loop counter, so array_splice($a, 1, -1) + // turned [1,2,3,4,5] into [1,1,1,1,1,1]. + let sliced = compile_and_run( + r#"= 80400` — the canonical feature-detection idiom — evaluates +// true, proving the constant participates in ordinary integer comparisons (finding F15). +/// Verifies that PHP version id supports feature-detection comparisons. +#[test] +fn test_php_version_id_feature_detection_idiom() { + let out = compile_and_run("= 80400 ? 'yes' : 'no';"); + assert_eq!(out, "yes"); +} + +// Tests `PHP_OS_FAMILY === 'Windows'` — the canonical cross-platform feature-detection +// idiom — evaluates against the native host, proving the constant is a real string +// usable in strict comparisons, not just echo (finding F15). +/// Verifies that PHP os family supports strict-equality feature-detection idiom. +#[test] +fn test_php_os_family_feature_detection_idiom() { + let out = compile_and_run(" 1000000000` is the only bound the fixture above places on the value, and + // both ways this has been wrong sail through it. The AArch64 helper asked the + // Darwin trap to fill a timeval, which it does not do -- that is libsystem's job + // -- so the struct was read back holding whatever the stack carried, once + // observed as 3.67e12. Routing through libc then exposed a second fault: Darwin's + // suseconds_t is 32 bits, so a 64-bit load of tv_usec folded in four bytes of + // padding libc does not clear, putting the result 2**32 microseconds -- 4294.97 + // seconds -- past the real time whenever that padding held 1. + // + // Both are caught by requiring agreement with time(), which samples the same + // clock. The window absorbs the sampling gap and a second boundary between the + // two calls; it does not absorb 4294 seconds, nor a value from another era. + let out = compile_and_run( + r#" -2.0 && $drift < 2.0) ? "agrees" : "drift:" . $drift; +"#, + ); + assert_eq!(out, "agrees"); +} + // Tests `microtime()` / `microtime(false)` return the "0.NNNNNNNN sec" string form. /// Verifies that the string form begins with "0." and carries the separating space at the fixed /// position 10 (after "0." plus eight fractional digits), for both the omitted-argument and @@ -2277,12 +2413,19 @@ fn test_usleep_zero() { // -- v0.8 getenv -- -// Tests `getenv("HOME")` returns a non-empty string on the current platform. -/// Verifies that getenv home. +// Tests the target's conventional home-directory variable is non-empty. +/// Verifies that `getenv` reads `HOME` on Unix and `USERPROFILE` on Windows. #[test] fn test_getenv_home() { - let out = - compile_and_run(" 0) { echo \"ok\"; }"); + let variable = if target().platform == Platform::Windows { + "USERPROFILE" + } else { + "HOME" + }; + let source = format!( + " 0) {{ echo \"ok\"; }}" + ); + let out = compile_and_run(&source); assert_eq!(out, "ok"); } @@ -2367,7 +2510,14 @@ if ( } "#, ); - assert_eq!(out, format!("{}\nok", target().platform.php_os_name())); + // PHP_OS is "WINNT", while php_uname('s') follows Windows' uname-style + // system label and reports "Windows NT" (the two PHP surfaces differ). + let system_name = if target().platform == Platform::Windows { + "Windows NT" + } else { + target().platform.php_os_name() + }; + assert_eq!(out, format!("{system_name}\nok")); } // Tests that `php_uname("sn")` (2-character mode string) fails at runtime with a @@ -2411,7 +2561,12 @@ fn test_exec() { #[test] fn test_system() { let out = compile_and_run(" PHP_INT_MAX) { echo 'range'; return; } if ($v !== 0) { $hit++; } } \ + echo $hit > 0 ? 'ok' : 'always-zero';", + ); + assert_eq!(out, "ok"); +} + +/// Verifies the 32-bit/64-bit sampling boundary: a span of exactly 2^32 must not +/// collapse. `random_int(0, 4294967295)` has 2^32 admissible values, one past what +/// a uint32 exclusive bound can express, and used to return zero every time. +#[test] +fn test_random_int_spans_uint32_boundary() { + let out = compile_and_run( + " 4294967295) { echo 'range'; return; } if ($v !== 0) { $hit++; } } \ + echo $hit > 0 ? 'ok' : 'always-zero';", + ); + assert_eq!(out, "ok"); +} + +/// Verifies the widest possible range stays in bounds. `PHP_INT_MAX - PHP_INT_MIN` +/// is `UINT64_MAX`, so forming an exclusive bound would wrap to zero; the helper +/// takes an inclusive width precisely to keep this case representable. +#[test] +fn test_random_int_full_width_range_stays_in_bounds() { + let out = compile_and_run( + " PHP_INT_MAX) { echo 'range'; return; } } echo 'ok';", + ); + assert_eq!(out, "ok"); +} + +/// Verifies `mt_rand()` over a wide range is sampled with the same 64-bit path, +/// since `rand`, `mt_rand` and `random_int` share one lowering. +#[test] +fn test_mt_rand_wide_range_is_not_always_zero() { + let out = compile_and_run( + " 0 ? 'ok' : 'always-zero';", + ); + assert_eq!(out, "ok"); +} + +/// Verifies a negative inclusive range remains in bounds and both endpoints are +/// reachable, covering the `min` re-addition after the unsigned sampling step. +#[test] +fn test_random_int_negative_range_in_bounds() { + let out = compile_and_run( + " 0) { echo 'range'; return; } if ($v === -1) { $lo = true; } \ + if ($v === 0) { $hi = true; } } echo ($lo && $hi) ? 'ok' : 'skewed';", + ); + assert_eq!(out, "ok"); +} + /// Verifies `rand()` with no arguments does not crash and returns a non-negative integer. #[test] fn test_rand_no_args() { @@ -113,6 +178,31 @@ fn test_number_format_with_decimals() { assert_eq!(out, "1,234.57"); } +/// Verifies PHP's decimal pre-rounding fixes binary representation edges before formatting. +#[test] +fn test_number_format_php_rounding_edges() { + let out = compile_and_run( + " 1000000000 ? "time" : "bad"; echo ":"; echo phpversion(); echo ":"; -echo sys_get_temp_dir(); echo ":"; +echo strlen(sys_get_temp_dir()) > 0 ? "tmp" : "bad"; echo ":"; echo strlen(getcwd()) > 0 ? "cwd" : "bad"; echo ":"; echo call_user_func("time") > 1000000000 ? "call-time" : "bad"; echo ":"; echo call_user_func("phpversion"); echo ":"; echo call_user_func_array("getcwd", []) !== "" ? "call-cwd" : "bad"; echo ":"; -echo call_user_func_array("sys_get_temp_dir", []); echo ":"; +echo call_user_func_array("sys_get_temp_dir", []) === sys_get_temp_dir() ? "call-tmp" : "bad"; echo ":"; echo function_exists("time"); echo function_exists("phpversion"); echo function_exists("getcwd"); echo function_exists("sys_get_temp_dir");'); "#, @@ -7032,7 +7062,7 @@ echo function_exists("sys_get_temp_dir");'); assert_eq!( out, format!( - "time:{}:/tmp:cwd:call-time:{}:call-cwd:/tmp:1111", + "time:{}:tmp:cwd:call-time:{}:call-cwd:call-tmp:1111", env!("CARGO_PKG_VERSION"), env!("CARGO_PKG_VERSION") ) @@ -7165,19 +7195,25 @@ echo function_exists("strtotime");'); ); } -/// Verifies eval `microtime()` returns a plausible floating timestamp by all call paths. +/// Verifies eval `microtime()` returns a string by default and a float when asked. +/// +/// php returns `" "` unless the argument is truthy +/// (ext/standard/microtime.c `_php_math_microtime`), so the default result is a +/// string and `microtime() > 1000000000` is false — it compares a non-numeric +/// string against a number. The fixture asserted that comparison was true, which +/// only held while eval returned a float from every call path. #[test] fn test_eval_dispatches_microtime_builtin_call() { let out = compile_and_run( r#" 1000000000 ? "now" : "bad"; echo ":"; -echo microtime(as_float: false) > 1000000000 ? "named" : "bad"; echo ":"; +eval('echo is_string(microtime()) ? "string" : "bad"; echo ":"; +echo is_string(microtime(as_float: false)) ? "named-string" : "bad"; echo ":"; echo call_user_func("microtime", true) > 1000000000 ? "call" : "bad"; echo ":"; echo call_user_func_array("microtime", ["as_float" => true]) > 1000000000 ? "array" : "bad"; echo ":"; echo function_exists("microtime");'); "#, ); - assert_eq!(out, "now:named:call:array:1"); + assert_eq!(out, "string:named-string:call:array:1"); } /// Verifies eval realpath-cache builtins expose elephc's empty-cache convention. @@ -7333,18 +7369,24 @@ echo function_exists("getprotobyname"); echo function_exists("getprotobynumber") /// Verifies eval stream introspection builtins return native-compatible static lists. #[test] fn test_eval_dispatches_stream_introspection_builtin_calls() { - let out = compile_and_run( + let (transport_count, transport_index, call_transport_index) = + if target().platform == Platform::Windows { + (8, 4, 7) + } else { + (10, 6, 9) + }; + let out = compile_and_run(&format!( r#" $tmp]) ? "calllock" : "bad"; echo ":"; echo function_exists("stream_get_wrappers"); echo function_exists("stream_get_transports"); echo function_exists("stream_get_filters"); echo function_exists("stream_is_local"); echo function_exists("stream_supports_lock");'); -"#, - ); +"# + )); assert_eq!( out, - "11:file:https:12:tcp:tlsv1.0:14:string.rot13:glob:tlsv1.3:bzip2.decompress:local:lock:calllocal:calllock:11111" + format!( + "11:file:https:{transport_count}:tcp:tlsv1.0:14:string.rot13:glob:tlsv1.3:bzip2.decompress:local:lock:calllocal:calllock:11111" + ) ); } @@ -7429,9 +7473,9 @@ echo ":"; echo function_exists("realpath");'); fn test_eval_dispatches_stream_resolve_include_path_builtin_call() { let out = compile_and_run( r#" "elephc-magician-missing-path"]) === false ? "array-false" : "bad"; echo ":"; echo function_exists("stream_resolve_include_path");'); "#, @@ -7851,7 +7895,12 @@ echo ":"; echo function_exists("disk_free_space"); echo function_exists("disk_to /// Verifies eval stat metadata builtins return scalar metadata and dispatch dynamically. #[test] fn test_eval_dispatches_stat_metadata_builtin_calls() { - let out = compile_and_run( + let executable_path = if target().platform.php_os_family() == "Windows" { + "C:/windows/system32/cmd.exe" + } else { + "/bin/sh" + }; + let source = format!( r#" 0 ? "mtime" : "bad"; echo ":"; @@ -7863,7 +7912,7 @@ echo filegroup("eval-stat.txt") >= 0 ? "group" : "bad"; echo ":"; echo fileinode("eval-stat.txt") > 0 ? "inode" : "bad"; echo ":"; echo filetype("eval-stat.txt") . ":"; echo filetype(".") . ":"; -echo is_executable("/bin/sh") ? "exec" : "bad"; echo ":"; +echo is_executable("{executable_path}") ? "exec" : "bad"; echo ":"; echo is_link("eval-stat.txt") ? "bad" : "notlink"; echo ":"; echo fileatime("missing-stat.txt") === false ? "missing-atime" : "bad"; echo ":"; echo filetype("missing-stat.txt") === false ? "missing-type" : "bad"; echo ":"; @@ -7876,8 +7925,9 @@ echo function_exists("fileowner"); echo function_exists("filegroup"); echo function_exists("fileinode"); echo function_exists("filetype"); echo function_exists("is_executable"); echo function_exists("is_link"); unlink("eval-stat.txt");'); -"#, +"# ); + let out = compile_and_run(&source); assert_eq!( out, "mtime:atime:ctime:perms:owner:group:inode:file:dir:exec:notlink:missing-atime:missing-type:missing-mtime:file:callinode:1111111111" @@ -8048,6 +8098,9 @@ echo function_exists("glob"); /// Verifies eval file-modification builtins update modes, masks, temp files, and dispatch. #[test] fn test_eval_dispatches_file_modify_builtin_calls() { + if target().platform == Platform::Windows { + return; + } let out = compile_and_run( r#" "printf q", "mode" => "r"]); +$callPipe = call_user_func_array("popen", ["command" => "{read_q_command}", "mode" => "r"]); echo fread($callPipe, 1) . ":"; echo call_user_func("pclose", $callPipe) . ":"; echo function_exists("tmpfile"); echo function_exists("popen"); echo function_exists("pclose");'); -"#, +"# ); + let out = compile_and_run(&source); assert_eq!(out, "tmpfile:3:abc:xyz:0:calltmp:q:0:111"); } @@ -9014,10 +9094,15 @@ echo eval('return function_exists("define") && function_exists("defined") ? "Y" /// Verifies eval can read predefined runtime constants and protect them from redefinition. #[test] fn test_eval_reads_predefined_runtime_constants() { + // The constants eval reports have to be the target's, not the host's. This + // fixture used to hardcode the POSIX values and passed on windows only because + // eval answered "Linux", "\n" and "/" there regardless of the target -- the very + // asymmetry against the compiled constants that has since been fixed. Each + // branch now names what php defines for its own platform. let out = compile_and_run_capture( r#" 9000000000000000000 ? "int" : "bad") . ":" . (defined("PHP_OS") ? "defined" : "bad") . ":" . @@ -9027,7 +9112,15 @@ echo eval('return (PHP_EOL === "\n" ? "eol" : "bad") . ":" . "#, ); assert!(out.success, "program failed: {}", out.stderr); - assert_eq!(out.stdout, "eol:os:/:int:defined:root:case:locked"); + let separator = if target().platform == Platform::Windows { + "\\" + } else { + "/" + }; + assert_eq!( + out.stdout, + format!("eol:os:{separator}:int:defined:root:case:locked") + ); assert!( out.stderr .contains("Warning: define(): Constant already defined"), @@ -9108,16 +9201,20 @@ fn test_eval_parse_error_reports_eval_parse_diagnostic() { ); } -/// Verifies eval failure classes map to distinct stable user-facing diagnostics. +/// Verifies eval parse failures remain distinct and never leak Rust panics. #[test] -fn test_eval_error_contract_distinguishes_parse_unsupported_runtime_and_warning() { +fn test_eval_error_contract_reports_parse_failure() { let parse_err = compile_and_run_expect_failure("hasMethod("from") ? "bad" : "nofrom";'); ); } +/// Compiles one invalid eval declaration and verifies the shared runtime fatal diagnostic. +fn assert_eval_runtime_failure(source: &str) { + let err = compile_and_run_expect_failure(source); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {err}" + ); +} + /// Verifies eval enums support user interfaces derived from PHP enum marker interfaces. #[test] -fn test_eval_declared_enum_marker_interface_inheritance() { +fn test_eval_declared_enum_marker_interface_inheritance_succeeds() { let out = compile_and_run( r#"value;'); out, "UB2:EvalDynUnitMarker:UnitEnum:3:EvalDynBackedMarker:UnitEnum:BackedEnum:ready" ); +} - let err = compile_and_run_expect_failure( +/// Verifies eval enums cannot explicitly implement PHP's `UnitEnum` marker. +#[test] +fn test_eval_declared_enum_rejects_explicit_unit_enum_implementation() { + assert_eval_runtime_failure( r#" 0) { + throw new Exception("boom"); + } +} catch (Throwable $error) { + echo "caught"; +} +"#, + &dir, + 8_388_608, + false, + false, + ); + + assert!( + user_asm + .lines() + .any(|line| line.contains("cbz x0") && line.contains("try_handler_resume")) + && user_asm + .lines() + .any(|line| { + line.trim_start().starts_with("b _eir_") + && line.contains("try_catch_dispatch") + }), + "AArch64 EIR try handlers must use a nearby zero-resume branch before the long handler jump:\n{user_asm}" + ); + assert!( + !user_asm + .lines() + .any(|line| line.contains("cbnz x0") && line.contains("try_catch_dispatch")), + "AArch64 EIR try handlers must not emit a direct range-limited longjmp branch:\n{user_asm}" + ); +} + /// Verifies eval class-relation builtins materialize generated/AOT metadata. #[test] fn test_eval_class_relation_builtins_expose_aot_metadata() { @@ -14610,9 +14792,9 @@ echo count($implements) . ":" . $implements["EvalDynNamedReader"] . ":" . $imple ); } -/// Verifies eval-declared method overrides enforce covariant return types. +/// Verifies eval-declared method overrides accept covariant return types. #[test] -fn test_eval_declared_method_return_type_override_contracts() { +fn test_eval_declared_method_return_type_override_accepts_covariance() { let out = compile_and_run_capture( r#"id();'); out.stdout, out.stderr ); assert_eq!(out.stdout, "2"); +} - let err = compile_and_run_expect_failure( +/// Verifies eval-declared overrides cannot widen a non-null return to nullable. +#[test] +fn test_eval_declared_method_return_type_override_rejects_wider_nullable() { + assert_eval_declared_method_return_type_runtime_failure( r#"maybeInt(null) === null ? "null" : "bad";'); out.stdout, out.stderr ); assert_eq!(out.stdout, "7:mixed:ok:null"); +} - let err = compile_and_run_expect_failure( +/// Verifies eval-declared method overrides reject an incompatible parameter type. +#[test] +fn test_eval_declared_method_parameter_override_rejects_incompatible_type() { + assert_eval_runtime_failure( r#"read();'); out.stdout, out.stderr ); assert_eq!(out.stdout, "7"); +} - let err = compile_and_run_expect_failure( +/// Verifies implementations cannot omit an eval-declared interface return type. +#[test] +fn test_eval_declared_interface_return_type_rejects_missing_implementation_type() { + assert_eval_runtime_failure( r#"read(8);'); out.stdout, out.stderr ); assert_eq!(out.stdout, "8:ok"); +} - let err = compile_and_run_expect_failure( +/// Verifies implementations cannot substitute an incompatible interface parameter type. +#[test] +fn test_eval_declared_interface_parameter_type_rejects_incompatible_type() { + assert_eval_runtime_failure( r#"done();'); out.stdout, out.stderr ); assert_eq!(out.stdout, "12:EvalReturnRuntimeBase:"); +} - let err = compile_and_run_expect_failure( +/// Verifies eval-declared scalar return types reject incompatible values. +#[test] +fn test_eval_declared_method_return_type_values_reject_bad_scalar() { + assert_eval_declared_method_return_type_runtime_failure( r#"id();'); "#, ); - assert!( - err.contains("Fatal error: eval() runtime failed"), - "stderr did not contain eval runtime fatal diagnostic: {err}" - ); +} - let err = compile_and_run_expect_failure( +/// Verifies eval-declared void return types reject explicit values. +#[test] +fn test_eval_declared_method_return_type_values_reject_explicit_void_value() { + assert_eval_declared_method_return_type_runtime_failure( r#"done();'); "#, ); - assert!( - err.contains("Fatal error: eval() runtime failed"), - "stderr did not contain eval runtime fatal diagnostic: {err}" - ); +} - let err = compile_and_run_expect_failure( +/// Verifies eval-declared static returns enforce the runtime receiver class. +#[test] +fn test_eval_declared_method_return_type_values_reject_bad_static_class() { + assert_eval_declared_method_return_type_runtime_failure( r#"make();'); "#, ); - assert!( - err.contains("Fatal error: eval() runtime failed"), - "stderr did not contain eval runtime fatal diagnostic: {err}" - ); +} - let err = compile_and_run_expect_failure( +/// Verifies eval-declared nullable scalar returns reject implicit fallthrough. +#[test] +fn test_eval_declared_method_return_type_values_reject_implicit_return() { + assert_eval_declared_method_return_type_runtime_failure( r#"id();'); "#, ); +} + +/// Compiles one eval method-return violation and checks the runtime-fatal contract. +fn assert_eval_declared_method_return_type_runtime_failure(source: &str) { + let err = compile_and_run_expect_failure(source); assert!( err.contains("Fatal error: eval() runtime failed"), "stderr did not contain eval runtime fatal diagnostic: {err}" @@ -15998,9 +16189,9 @@ echo $box->value;'); assert_eq!(out.stdout, "1:3:3"); } -/// Verifies eval-declared readonly properties can be initialized only in constructors. +/// Verifies eval-declared readonly properties can be initialized in constructors. #[test] -fn test_eval_declared_readonly_property_rules() { +fn test_eval_declared_readonly_property_initializes_in_constructor() { let out = compile_and_run_capture( r#"id();'); out.stdout, out.stderr ); assert_eq!(out.stdout, "7"); +} +/// Verifies eval-declared readonly properties reject writes after construction. +#[test] +fn test_eval_declared_readonly_property_rejects_second_write() { let err = compile_and_run_capture( r#"name . ":" . $box->value . ":" . $plain->value;'); out.stdout, out.stderr ); assert_eq!(out.stdout, "box:Ada!:Grace"); +} - let err = compile_and_run_expect_failure( +/// Verifies an interface read/write hook cannot be implemented by a read-only hook. +#[test] +fn test_eval_interface_property_hook_rejects_missing_setter() { + assert_eval_interface_property_hook_contract_failure( r#"label;'); assert_eq!(out, "AB:XY"); } -/// Verifies eval ReflectionClass::newInstance rejects non-instantiable AOT class-likes. +/// Verifies eval ReflectionClass::newInstance rejects an abstract AOT class. #[test] -fn test_eval_reflection_class_new_instance_rejects_aot_non_instantiable_class_likes() { - let cases = [ - ( - "abstract class EvalReflectNewAotAbstract {}", - "EvalReflectNewAotAbstract", - "Error:Cannot instantiate abstract class EvalReflectNewAotAbstract", - ), - ( - "interface EvalReflectNewAotIface {}", - "EvalReflectNewAotIface", - "Error:Cannot instantiate interface EvalReflectNewAotIface", - ), - ( - "trait EvalReflectNewAotTrait {}", - "EvalReflectNewAotTrait", - "Error:Cannot instantiate trait EvalReflectNewAotTrait", - ), - ( - "enum EvalReflectNewAotEnum { case Ready; }", - "EvalReflectNewAotEnum", - "Error:Cannot instantiate enum EvalReflectNewAotEnum", - ), - ]; - for (declaration, class_name, expected) in cases { - let source = format!( - r#"getMessage(); }}'); "# - ); - let out = compile_and_run(&source); - assert_eq!(out, expected, "unexpected stdout for {class_name}"); - } + ); + let out = compile_and_run(&source); + assert_eq!(out, expected, "unexpected stdout for {class_name}"); } /// Verifies eval ReflectionClass instantiation rejects eval non-instantiable class-likes like PHP. @@ -26505,34 +26806,54 @@ echo $ref->isInstantiable() ? "I" : "i";'); assert_eq!(out, "4:i"); } -/// Verifies eval ReflectionClass::newInstanceWithoutConstructor rejects non-allocatable AOT class-likes. +/// Verifies newInstanceWithoutConstructor rejects an abstract AOT class. #[test] -fn test_eval_reflection_class_new_instance_without_constructor_rejects_aot_non_classes() { - let cases = [ - ( - "abstract class EvalReflectNoCtorAotAbstract {}", - "EvalReflectNoCtorAotAbstract", - "Error:Cannot instantiate abstract class EvalReflectNoCtorAotAbstract", - ), - ( - "interface EvalReflectNoCtorAotIface {}", - "EvalReflectNoCtorAotIface", - "Error:Cannot instantiate interface EvalReflectNoCtorAotIface", - ), - ( - "trait EvalReflectNoCtorAotTrait {}", - "EvalReflectNoCtorAotTrait", - "Error:Cannot instantiate trait EvalReflectNoCtorAotTrait", - ), - ( - "enum EvalReflectNoCtorAotEnum { case Ready; }", - "EvalReflectNoCtorAotEnum", - "Error:Cannot instantiate enum EvalReflectNoCtorAotEnum", - ), - ]; - for (declaration, class_name, expected) in cases { - let source = format!( - r#"getMessage(); }}'); "# - ); - let out = compile_and_run(&source); - assert_eq!(out, expected, "unexpected stdout for {class_name}"); - } + ); + let out = compile_and_run(&source); + assert_eq!(out, expected, "unexpected stdout for {class_name}"); } /// Verifies eval ReflectionClassConstant/EnumCase expose eval-declared attributes. diff --git a/tests/codegen/eval_builtin_parity.rs b/tests/codegen/eval_builtin_parity.rs index 5ec9c24ed5..1c3e346bef 100644 --- a/tests/codegen/eval_builtin_parity.rs +++ b/tests/codegen/eval_builtin_parity.rs @@ -10,7 +10,7 @@ use std::fmt::Write; -use crate::support::{compile_and_run, compile_and_run_capture}; +use crate::support::{compile_and_run, compile_and_run_capture, target, Platform}; const STATIC_ONLY_REGISTRY_BUILTINS: &[&str] = &[ "array_all", @@ -105,6 +105,9 @@ fn test_eval_function_exists_covers_static_builtin_catalog() { if STATIC_ONLY_REGISTRY_BUILTINS.contains(name) { continue; } + if target().platform == Platform::Windows && matches!(*name, "lchgrp" | "lchown") { + continue; + } writeln!( &mut fragment, "if (!function_exists(\"{name}\")) {{ echo \"{name},\"; }}" diff --git a/tests/codegen/eval_callable_ref_errors.rs b/tests/codegen/eval_callable_ref_errors.rs index 43430dc3aa..7f3a06d7b7 100644 --- a/tests/codegen/eval_callable_ref_errors.rs +++ b/tests/codegen/eval_callable_ref_errors.rs @@ -69,10 +69,9 @@ try { ); } -/// Verifies AOT function argument-prep fatals restore the eval bridge frame. -#[test] -fn test_eval_aot_function_by_ref_arg_prep_fatal_cleans_up_stack() { - let cases = [ +/// Returns the AOT function callable variants that must fail during argument preparation. +fn eval_aot_function_by_ref_arg_prep_fatal_cases() -> [(&'static str, &'static str); 4] { + [ ( "string callable", r#"loop(); /// Regression: FFI extern `poll` called after a loop with internal function calls must not clobber the local `int` `$nfds` argument. #[test] fn test_ffi_extern_poll_after_loop_with_calls_preserves_local_int_arg() { + if target().platform == Platform::Windows { + eprintln!("skipping POSIX poll fixture on Windows; Winsock exposes WSAPoll instead"); + return; + } // Regression: FFI extern `poll` called after a loop with internal function calls must not clobber the local `int` `$nfds` argument. let out = compile_and_run( r#" 0 ? "yes" : "no"; /// Verifies that an extern function with a `string` parameter and `string` return type /// is resolved and called correctly. /// -/// Fixture: extern function getenv("HOME") returning string, asserts strlen > 0. +/// Fixture: extern function getenv("PATH") returning string, asserts strlen > 0. #[test] fn test_ffi_extern_string_return() { let out = compile_and_run( r#" 0 ? "ok" : "empty"; +$path = getenv("PATH"); +echo strlen($path) > 0 ? "ok" : "empty"; "#, ); assert_eq!(out, "ok"); diff --git a/tests/codegen/ffi/syntax_and_callbacks.rs b/tests/codegen/ffi/syntax_and_callbacks.rs index 4723aed754..ef62b49840 100644 --- a/tests/codegen/ffi/syntax_and_callbacks.rs +++ b/tests/codegen/ffi/syntax_and_callbacks.rs @@ -119,6 +119,10 @@ strcmp("bb", "bb"); /// Verifies `extern global ptr $environ` declares an FFI external global pointer variable. #[test] fn test_ffi_extern_global() { + if target().platform == Platform::Windows { + eprintln!("skipping POSIX environ global fixture on Windows"); + return; + } let out = compile_and_run( r#"start("A", "B", "C", "D"); /// silently corrupting the heap. The program must not reach "should-not-reach". #[test] fn test_fiber_stack_overflow_faults_via_guard_page() { - // The fiber stack now has a 16 KB PROT_NONE guard page at its bottom. A - // runaway recursion inside the fiber must trip that page and terminate - // with SIGSEGV/SIGBUS instead of silently corrupting the heap. + // The fiber stack now has a 16 KB PROT_NONE guard page at its bottom. Keep + // work per frame scalar-only and retain a post-call effect to prevent tail + // recursion, so slow Wine hosts reach the guard page deterministically. let out = compile_and_run_capture( r#" 1000000) return $n; - return recurse($n + 1); + $result = recurse($n + 1); + if ($result < 0) echo "unreachable"; + return $result; } $f = new Fiber(function(): void { recurse(0); diff --git a/tests/codegen/io.rs b/tests/codegen/io.rs index 3ca0f5366f..76312a2665 100644 --- a/tests/codegen/io.rs +++ b/tests/codegen/io.rs @@ -21,6 +21,8 @@ mod streams; mod filesystem; #[path = "io/misc.rs"] mod misc; +#[path = "io/proc.rs"] +mod proc; #[path = "io/stat_ext.rs"] mod stat_ext; #[path = "io/paths/mod.rs"] diff --git a/tests/codegen/io/filesystem.rs b/tests/codegen/io/filesystem.rs index afd93ef6c2..38826e8a8d 100644 --- a/tests/codegen/io/filesystem.rs +++ b/tests/codegen/io/filesystem.rs @@ -99,8 +99,13 @@ if (strlen($cwd) > 0) { echo "ok"; } assert_eq!(out, "ok"); } -/// Verifies sys_get_temp_dir returns a path containing "tmp" (case-insensitive -/// check to cover Linux, macOS, and Windows temp naming). +/// Verifies sys_get_temp_dir names a directory that exists. +/// +/// The path is not necessarily spelled "tmp" or "temp": php resolves TMPDIR, and +/// macOS points it at a per-user `/var/folders//T` directory whose name +/// contains neither. That spelling only held while the lowering hardcoded `/tmp`. +/// On Windows the runtime returns the `GetTempPathW` result, which the host cannot +/// stat when the binary runs under Wine, so the name check is kept there. #[test] fn test_sys_get_temp_dir() { let out = compile_and_run( @@ -109,7 +114,20 @@ $tmp = sys_get_temp_dir(); echo $tmp; "#, ); - assert!(out.contains("tmp") || out.contains("Tmp")); + assert!(!out.is_empty(), "sys_get_temp_dir returned an empty path"); + if cfg!(windows) { + assert!( + out.to_lowercase().contains("temp"), + "sys_get_temp_dir returned {:?}", + out + ); + } else { + assert!( + std::path::Path::new(&out).is_dir(), + "sys_get_temp_dir returned {:?}, which is not a directory", + out + ); + } } /// Verifies chdir changes the working directory and getcwd reflects the new @@ -229,6 +247,42 @@ unlink($tmp); let _ = fs::remove_dir_all(&dir); } +/// Verifies `tempnam` keeps only the basename of a long prefix and applies PHP's 63-byte limit. +#[test] +fn test_tempnam_normalizes_and_limits_prefix() { + let (out, dir) = compile_and_run_in_dir( + r#" 3 && $dir[1] === ":" && substr($dir, -1) !== "\\" && is_dir($dir)) + ? "absolute-unsuffixed" : "[" . $dir . "]"; +"#, + ); + assert_eq!(resolved, "absolute-unsuffixed"); + return; + } + + let plain = compile_and_run( + r#" 1000000000 ? "stamped" : "bad"; +unlink($path); +"#, + ); + assert_eq!(out, "false|stamped"); +} + +/// Verifies a read larger than the concat arena does not destroy its surroundings. +#[test] +fn test_fread_beyond_the_concat_arena_keeps_its_surroundings() { + // The arena is a flat 64 KiB buffer with an offset and no capacity of its own, so + // a request larger than what is left of it was written straight past the end. + // Nothing reported it: the returned bytes were correct -- read back from the same + // overflowed region -- while whatever followed the buffer was destroyed. Here the + // casualty is the stream itself, which fclose() then rejected as "unknown given". + // + // The victim depends on the program's layout, which is what made this look + // intermittent: an earlier call that moved the arena offset moved the damage + // somewhere harmless. So the fixture pins both a neighbour and the handle. + // + // The payload is written in chunks rather than with str_repeat(200000), because + // building a string that size would route it through the very arena under test. + let out = compile_and_run( + r#" &'static str { + if target().platform == Platform::Windows { + "\\" + } else { + "/" + } +} + /// Verifies `basename()` extracts the filename from a simple absolute path. /// Fixture: `/etc/passwd` → expects `passwd`. #[test] @@ -135,20 +149,25 @@ fn test_dirname_no_separator() { assert_eq!(out, "."); } -/// Verifies `dirname()` on a direct child of root returns `/`. -/// Fixture: `/foo` → expects `/`. +/// Verifies `dirname()` on a direct child of root returns the platform root. +/// Fixture: `/foo` → expects `/` on POSIX and `\` on Windows. +/// +/// php-src rewrites the root result to `PHP_WIN32_IOUTIL_DEFAULT_SLASH` +/// (win32/ioutil.h defines it as `'\\'`), so PHP on Windows normalises the root +/// to a backslash even when the input used a forward slash. #[test] fn test_dirname_root_child() { let out = compile_and_run(r#" ["pipe", "r"], 1 => ["pipe", "w"]], $pipes); +echo $r === false ? "false" : "resource"; +"#, + ); + assert_eq!(out, "resource"); +} + +/// Verifies binary pipe mode suffixes keep php-src's first-byte direction +/// semantics: `rb` behaves as read mode and `wb` behaves as write mode. The +/// Windows filter may choose CRLF line endings, so only its payload prefix is +/// part of the cross-platform assertion. +#[test] +fn test_proc_open_accepts_binary_pipe_modes() { + let command = if target().platform == Platform::Windows { + "findstr .*" + } else { + "cat" + }; + let source = + r#" ["pipe", "rb"], 1 => ["pipe", "wb"]], + $pipes, +); +if ($process === false) { + echo "fail"; +} else { + fwrite($pipes[0], "binary-direction\n"); + fclose($pipes[0]); + $output = fread($pipes[1], 100); + fclose($pipes[1]); + proc_close($process); + echo substr($output, 0, 16) === "binary-direction" ? "ok" : "missing"; +} +"# + .replace("__COMMAND__", command); + let out = compile_and_run(&source); + assert_eq!(out, "ok"); +} + +/// Verifies PHP weakly coerces an integer pipe mode to a non-write-leading +/// string, so the parent receives the pipe's write end just as php-src does. +/// Windows filters may append CRLF, so the assertion compares the payload. +#[test] +fn test_proc_open_coerces_integer_pipe_mode_like_php() { + let command = if target().platform == Platform::Windows { + "findstr .*" + } else { + "cat" + }; + let source = + r#" ["pipe", 1], 1 => ["pipe", "w"]], $pipes); +if ($process === false) { + echo "false"; +} else { + fwrite($pipes[0], "scalar-mode\n"); + fclose($pipes[0]); + echo fread($pipes[1], 100); + fclose($pipes[1]); + proc_close($process); +} +"# + .replace("__COMMAND__", command); + let out = compile_and_run(&source); + assert_eq!( + out.trim_end_matches(&['\r', '\n'][..]), + "scalar-mode" + ); +} + +/// Verifies proc_close reaps the child and returns the exit status. `echo hi` +/// exits 0, so the close must return `0`. Replaces the former C1a compile-only +/// failure test. +#[test] +fn test_proc_close_returns_exit_status() { + let out = compile_and_run( + r#" ["pipe", "r"], 1 => ["pipe", "w"]], $pipes); +if ($r === false) { + echo "fail"; +} else { + fclose($pipes[0]); + fread($pipes[1], 100); + fclose($pipes[1]); + echo proc_close($r); +} +"#, + ); + assert_eq!(out, "0"); +} + +/// Verifies a live process reports PHP's full non-cached status shape before +/// `proc_terminate` forwards SIGTERM on Unix. The explicit close then reaps the +/// child and exercises status-registry teardown after a prior status lookup. +#[test] +fn test_proc_get_status_then_terminate() { + let output = compile_and_run_capture( + r#" ["pipe", "w"]], $pipes); +if ($process === false) { + echo "false"; +} else { + $status = proc_get_status($process); + echo ($status["running"] ? "running" : "stopped"); + echo ":" . ($status["cached"] ? "cached" : "fresh"); + echo proc_terminate($process) ? ":terminated" : ":failed"; + proc_close($process); +} +"#, + ); + assert!( + output.success, + "proc_get_status process exited unsuccessfully: stdout={:?}, stderr={:?}", + output.stdout, + output.stderr + ); + assert_eq!(output.stdout, "running:fresh:terminated"); +} + +/// Verifies `proc_terminate` forwards its default SIGTERM without requiring a +/// preceding status query, then lets `proc_close` reap the terminated child. +#[test] +fn test_proc_terminate_reaps_live_child() { + let out = compile_and_run( + r#" ["pipe", "w"]], $pipes); +if ($process === false) { + echo "false"; +} else { + echo proc_terminate($process) ? "terminated" : "failed"; + proc_close($process); +} +"#, + ); + assert_eq!(out, "terminated"); +} + +/// Verifies weak PHP typing coerces a numeric string signal before terminating the child. +#[test] +fn test_proc_terminate_coerces_numeric_string_signal() { + let out = compile_and_run( + r#" ["pipe", "w"]], $pipes); +if ($process === false) { + echo "false"; +} else { + echo proc_terminate($process, "15") ? "terminated" : "failed"; + proc_close($process); +} +"#, + ); + assert_eq!(out, "terminated"); +} + +/// Verifies a runtime-produced numeric string uses strict PHP int-parameter coercion. +#[test] +fn test_proc_terminate_coerces_dynamic_numeric_string_signal() { + let out = compile_and_run( + r#" ["pipe", "w"]], $pipes); +if ($process === false) { + echo "false"; +} else { + echo proc_terminate($process, process_signal()) ? "terminated" : "failed"; + proc_close($process); +} +"#, + ); + assert_eq!(out, "terminated"); +} + +/// Verifies a runtime-produced non-numeric string throws PHP's catchable TypeError. +#[test] +fn test_proc_terminate_rejects_dynamic_non_numeric_string_signal() { + let output = compile_and_run_capture( + r#" ["pipe", "w"]], $pipes); +if ($process === false) { + echo "false"; +} else { + try { + proc_terminate($process, invalid_process_signal()); + echo "missing-error"; + } catch (TypeError $error) { + echo get_class($error) . ":" . $error->getMessage(); + proc_terminate($process); + } + proc_close($process); +} +"#, + ); + assert!( + output.success, + "dynamic signal TypeError was not caught: stdout={:?}, stderr={:?}", + output.stdout, + output.stderr + ); + assert_eq!( + output.stdout, + "TypeError:proc_terminate(): Argument #2 ($signal) must be of type int, string given" + ); +} + +/// Verifies a normal exit remains available to `proc_close` after a status +/// query. Unix caches the reaped wait status, while Windows keeps the process +/// handle live and deliberately reports `cached => false`, matching php-src. +#[test] +fn test_proc_get_status_normal_exit_preserves_proc_close_code() { + let out = compile_and_run( + r#" ["pipe", "w"]], $pipes); +if ($process === false) { + echo "false"; +} else { + usleep(100000); + $status = proc_get_status($process); + echo ($status["cached"] ? "cached" : "fresh"), ":", $status["exitcode"], ":", proc_close($process); +} +"#, + ); + assert_eq!( + out, + if cfg!(windows) { + "fresh:7:7" + } else { + "cached:7:7" + } + ); +} + +/// Reads back a pipe populated through `proc_open`'s by-reference `$pipes` +/// parameter. This protects the checker flow fact that an empty input array +/// becomes an integer-keyed associative array of stream resources after the +/// call, matching descriptor keys that promote packed storage to a hash. +#[test] +fn test_proc_open_pipe_readback() { + let out = compile_and_run( + r#" ["pipe", "w"]], $pipes); +if ($r === false) { echo "false"; } +else { + $s = fread($pipes[1], 100); + fclose($pipes[1]); + proc_close($r); + echo $s; +} +"#, + ); + assert!(out.contains("hi"), "out was: {}", out); +} + +/// Verifies the complete PHP parameter list accepts explicit null settings and +/// preserves named-argument mapping while using the implemented pipe runtime. +#[test] +fn test_proc_open_accepts_nullable_optional_settings() { + let out = compile_and_run( + r#" ["pipe", "r"]], + pipes: $pipes, + cwd: null, + env_vars: null, + options: null, +); +echo $r === false ? "false" : proc_close($r); +"#, + ); + assert_eq!(out, "0"); +} + +/// Verifies a descriptor set larger than the initial empty-array capacity +/// writes the reallocated `$pipes` container back through the by-ref local. +#[test] +fn test_proc_open_writes_back_reallocated_pipes() { + let out = compile_and_run( + r#"/dev/null" + }; + let out = compile_and_run( + &format!(r#" ["pipe", "w"]], + $pipes, +); +if ($process === false) {{ echo "fail"; }} +else {{ + proc_close($process); + echo "done"; +}} +"#, command), + ); + assert_eq!(out, "done"); +} + +/// Verifies a child exit code above 0x7FFFFFFF comes back as a negative integer. +/// +/// Windows exit codes span the whole DWORD range, and php stores the value +/// `GetExitCodeProcess()` produces into an `int` — `FG(pclose_ret)` for +/// `proc_close()` and `proc->exit_code` for `proc_get_status()` — so +/// `cmd /c exit -1` reads back as `-1`. Loading the DWORD with a zero-extending +/// move instead reported 4294967295, which compares wrong against every negative +/// exit-code check. POSIX shells clamp the status to 0..255, so the oversized case +/// only exists on Windows and the other targets assert the clamped value. +#[test] +fn test_proc_close_reports_negative_exit_code() { + let (command, expected) = if target().platform == Platform::Windows { + ("cmd /c exit -1", "-1") + } else { + ("sh -c 'exit 255'", "255") + }; + let out = compile_and_run( + &r#" ["pipe", "w"]], $pipes); +fclose($pipes[1]); +echo proc_close($process); +"# + .replace("__COMMAND__", command), + ); + assert_eq!(out, expected); +} + +/// Verifies `proc_get_status()` reports the same signed exit code as `proc_close()`, +/// since both read the value through `GetExitCodeProcess()` on Windows. +#[test] +fn test_proc_get_status_reports_negative_exit_code() { + let (command, expected) = if target().platform == Platform::Windows { + ("cmd /c exit -1", "-1") + } else { + ("sh -c 'exit 255'", "255") + }; + let out = compile_and_run( + &r#" ["pipe", "w"]], $pipes); +fclose($pipes[1]); +$status = proc_get_status($process); +while ($status["running"]) { $status = proc_get_status($process); } +echo $status["exitcode"]; +proc_close($process); +"# + .replace("__COMMAND__", command), + ); + assert_eq!(out, expected); +} diff --git a/tests/codegen/io/stat_ext.rs b/tests/codegen/io/stat_ext.rs index 564383c16f..0349e61efa 100644 --- a/tests/codegen/io/stat_ext.rs +++ b/tests/codegen/io/stat_ext.rs @@ -153,14 +153,10 @@ echo fileinode("missing.txt") === false ? "i" : "!"; assert_eq!(out, "acpogi"); } -/// Verifies `is_executable()` returns true for `/bin/sh`, which is executable on every -/// POSIX target the compiler ships for. Regression guard for target-specific path handling. +/// Verifies `is_executable()` recognizes the currently running native binary on every target. #[test] fn test_is_executable_true_for_self() { - // /bin/sh is executable on every POSIX target we ship for. - let out = compile_and_run( - r#"&1'); +echo str_contains($setup, "Failed processing 0 files") ? "setup:ok " : "setup:FAIL "; +echo file_exists($path) ? "exists:yes " : "exists:no "; +echo is_readable($path) ? "read:yes " : "read:no "; +echo is_writable($path) ? "write:yes " : "write:no "; +$undo = shell_exec('icacls "' . $path . '" /remove:d "%USERNAME%" 2>&1'); +echo str_contains($undo, "Failed processing 0 files") ? "undo:ok " : "undo:FAIL "; +echo is_writable($path) ? "write2:yes" : "write2:no"; +unlink($path); +"#, + ); + assert_eq!(out, "setup:ok exists:yes read:yes write:no undo:ok write2:yes"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies `is_readable()` honors a DACL that denies FILE_READ_DATA while +/// leaving READ_CONTROL intact, so the descriptor is still fetchable and the +/// denial is a real `AccessCheck` verdict rather than a fail-open. +#[test] +fn test_windows_acl_denied_read_is_not_readable() { + if target().platform != Platform::Windows || !cfg!(windows) { + return; + } + let (out, dir) = compile_and_run_in_dir( + r#"&1'); +echo str_contains($setup, "Failed processing 0 files") ? "setup:ok " : "setup:FAIL "; +echo file_exists($path) ? "exists:yes " : "exists:no "; +echo is_readable($path) ? "read:yes " : "read:no "; +echo is_writable($path) ? "write:yes " : "write:no "; +$undo = shell_exec('icacls "' . $path . '" /remove:d "%USERNAME%" 2>&1'); +echo str_contains($undo, "Failed processing 0 files") ? "undo:ok " : "undo:FAIL "; +echo is_readable($path) ? "read2:yes" : "read2:no"; +unlink($path); +"#, + ); + assert_eq!(out, "setup:ok exists:yes read:no write:yes undo:ok read2:yes"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies a protected system binary is executable and readable but not +/// writable, the php-parity answer. `cmd.exe` carries no readonly attribute, +/// so the attribute-only shim calls it writable; only the DACL says otherwise. +#[test] +fn test_windows_system_binary_is_readable_executable_not_writable() { + if target().platform != Platform::Windows || !cfg!(windows) { + return; + } + let (out, dir) = compile_and_run_in_dir( + r#"= 1 ? "n" : "N"; +echo $st["ino"] > 0 ? "i" : "I"; +echo $st["dev"] > 0 ? "d" : "D"; +echo $st["mtime"] > 0 ? "t" : "T"; +echo (($st["mode"] & 0o222) === 0) ? "r" : "R"; +fclose($h); +chmod("fstat-native.txt", 0o644); +unlink("fstat-native.txt"); +"#, + ); + assert_eq!(out, "snidtr"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies `stat()` reports the block fields PHP reports on the target. +/// +/// Windows has neither `st_blksize` nor `st_blocks`, so php-src compiles the +/// `#else` arms of ext/standard/filestat.c php_stat and reports -1 for both. +/// elephc left the zeroed stat buffer alone and reported 0, which reads as a real +/// answer: `if ($s['blksize'] > 0)` takes the wrong branch and a chunked-copy loop +/// sized from it divides by zero. POSIX block sizes vary by filesystem, so only +/// their sign is portable. +#[test] +fn test_stat_block_fields_match_platform() { + let (out, dir) = compile_and_run_in_dir( + r#" 0, "posix st_blksize must be positive, got {blksize}"); + assert!(blocks >= 0, "posix st_blocks must be non-negative, got {blocks}"); + } + let _ = fs::remove_dir_all(&dir); +} diff --git a/tests/codegen/io/streams.rs b/tests/codegen/io/streams.rs index 8ecf9d66a1..e6887726f3 100644 --- a/tests/codegen/io/streams.rs +++ b/tests/codegen/io/streams.rs @@ -9,6 +9,36 @@ use super::*; +/// Renders a host path for insertion into a PHP double-quoted string literal. +/// +/// Win32 accepts forward slashes, while raw Windows backslashes can form PHP +/// escapes such as `\r` and `\t` inside the generated source fixture. +fn php_fixture_path(path: &std::path::Path) -> String { + path.to_string_lossy().replace('\\', "/") +} + +/// Returns the stream-socket-pair domain that php-src supports on this target. +/// +/// Windows implements `socketpair()` only through its IPv4 loopback emulation; +/// Unix targets retain the native Unix-domain variant used by these fixtures. +fn socket_pair_fixture_domain() -> &'static str { + if target().platform == Platform::Windows { + "STREAM_PF_INET" + } else { + "STREAM_PF_UNIX" + } +} + +/// Verifies fixture paths never leave a PHP-recognized backslash escape in the +/// generated source, even when the host path uses Windows separators. +#[test] +fn php_fixture_path_uses_php_safe_forward_slashes() { + assert_eq!( + php_fixture_path(std::path::Path::new(r"C:\runner\Temp\archive.phar")), + "C:/runner/Temp/archive.phar" + ); +} + /// Verifies STDIN constant evaluates to the expected resource display string. #[test] fn test_stdin_constant() { @@ -121,6 +151,61 @@ echo ($empty === false ? "e" : "!"); assert_eq!(out.stderr, ""); } +/// Verifies `+` is recognized after either binary-mode spelling and permits read/write access. +#[test] +fn test_fopen_binary_read_write_mode_orders() { + let out = compile_and_run( + r#" (std::thread::JoinHandle<()>, u16) { + let listener = + std::net::TcpListener::bind(("127.0.0.1", 0)).expect("refusing peer: bind port"); + let port = listener.local_addr().expect("refusing peer: local addr").port(); + let handle = std::thread::spawn(move || { + if let Ok((sock, _)) = listener.accept() { + let _ = sock.shutdown(std::net::Shutdown::Both); + } + }); + (handle, port) +} + /// Minimal one-shot HTTPS server for deterministic `https://` wrapper tests. /// Binds an ephemeral port and returns it alongside the handle. fn spawn_https_server(content: &'static [u8]) -> (std::thread::JoinHandle<()>, u16) { @@ -4180,6 +4351,7 @@ fn test_file_get_contents_over_https_local_server() { let out = compile_and_run(&format!( r#": // $pair[0] / $pair[1] must yield the stored descriptors, not the index. - let out = compile_and_run( - r#"enable_on_connect` in the `tls`/`ssl` transport factory +/// (ext/openssl/openssl.c registers them through `php_stream_xport_register`). +/// +/// Against a real rustls server, a plaintext socket cannot produce a valid HTTP +/// response: the server aborts the connection on the malformed ClientHello. So a +/// readable body here is proof that the handshake actually ran. +#[test] +fn test_stream_socket_client_tls_scheme_performs_handshake() { + let (_server, port) = spawn_https_server(b"tls transport body"); + let out = compile_and_run(&format!( + r#"is_client`), so an untrusted certificate makes +/// `stream_socket_client()` return `false`. Every other test here disables +/// verification to reach the data path, which means none of them would notice a +/// handshake that negotiated TLS but authenticated nobody. +#[test] +fn test_stream_socket_client_tls_default_context_rejects_untrusted_cert() { + let (_server, port) = spawn_https_server(b"must not be reachable"); + let out = compile_and_run(&format!( + r#" "` string unless the argument is +/// truthy (ext/standard/microtime.c `_php_math_microtime`). The interpreter +/// evaluated the argument only for its side effects and always returned a float, so +/// an eval'd `microtime()` handed back a double where the compiled builtin -- which +/// already has the float, string and mixed variants -- returned a string. +#[test] +fn test_eval_microtime_honours_its_as_float_argument() { + let out = compile_and_run( + r#" u64 { + let (_output, dir) = compile_and_run_in_dir(source); + let binary = target_binary_path(&dir.join("test")); + let size = fs::metadata(&binary) + .unwrap_or_else(|error| { + panic!("linked binary {} could not be measured: {error}", binary.display()) + }) + .len(); + let _ = fs::remove_dir_all(&dir); + size +} + +/// Verifies a literal plaintext address does not drag the TLS bridge into the +/// binary, while a crypto scheme and a runtime address both still do. +/// +/// The scheme normally lives in a runtime string, so the handshake path — and with +/// it the bridge exports — has to be emitted. Reading a `ConstStr` address back at +/// lowering time lets a program that only ever says `tcp://` skip both: measured +/// here at 155 KiB against 5.7 MiB, a 37x difference that is entirely rustls. The +/// lowering and `stream_socket_client_requirements` decide on the same literal, so +/// they cannot disagree; were they to, the bridge exports would be undefined at +/// link time, which is loud rather than a silent plaintext connection. +#[test] +fn test_literal_plain_address_does_not_link_the_tls_bridge() { + let plain = compiled_binary_size(r#" bridge_floor, "a literal tls:// address must link the TLS bridge, got {crypto} bytes"); + assert!(dynamic > bridge_floor, "a runtime address must link the TLS bridge, got {dynamic} bytes"); +} + +/// Verifies `fsockopen()` negotiates the crypto transports, as php does. +/// +/// php accepts a crypto scheme in the hostname — `fsockopen("tls://host", 443)` +/// returns an encrypted stream — because fsockopen and stream_socket_client share +/// one transport factory. elephc reaches the socket through the `__rt_fsockopen` +/// runtime helper, which tail-calls `__rt_stream_socket_client`, so it bypassed the +/// crypto-on-connect block that lives in the stream_socket_client lowering and kept +/// handing back a plaintext socket for a `tls://` hostname. +#[test] +fn test_fsockopen_tls_hostname_performs_handshake() { + let (_server, port) = spawn_https_server(b"fsockopen over tls"); + let out = compile_and_run(&format!( + r#" bool { + if target().platform == Platform::Windows { + let bytes = path.as_bytes(); + return (bytes.len() >= 3 + && bytes[0].is_ascii_alphabetic() + && bytes[1] == b':' + && matches!(bytes[2], b'\\' | b'/')) + || path.starts_with(r"\\"); + } + std::path::Path::new(path).is_absolute() +} + /// Verifies `__FILE__` is an absolute path ending in `test.php`. The temp /// directory provides an absolute path; __FILE__ is substituted at lowering. #[test] fn test_dunder_file_is_absolute_path_ending_in_test_php() { let out = compile_and_run(" $b); ); assert_eq!(out, "101-1"); } + +/// Verifies that a program using ONLY the OOP datetime surface — with no procedural +/// `date()`/`mktime()`/`strtotime()` call anywhere — still resolves IANA zone offsets. +/// +/// This is the regression guard for the windows elephc-tz bridge requirement: the +/// synthetic OOP method bodies never flow through the builtin `check` hooks, so without +/// the call-site gate in `require_datetime_tz_bridge_if_needed` such a program links +/// without `-lelephc_tz` and fails at link time on `elephc_tz_offset`. +#[test] +fn test_oop_only_datetime_resolves_tz_offsets() { + let out = compile_and_run( + r#"getOffset($d), "\n"; + +$tzParis = new DateTimeZone("Europe/Paris"); +$dParis = new DateTime("2024-01-15 12:00:00", $tzParis); +echo $tzParis->getOffset($dParis), "\n"; +"#, + ); + assert_eq!(out, "0\n3600\n"); +} diff --git a/tests/codegen/oop/modifiers_and_properties.rs b/tests/codegen/oop/modifiers_and_properties.rs index a790cfa7f2..edcc939e0f 100644 --- a/tests/codegen/oop/modifiers_and_properties.rs +++ b/tests/codegen/oop/modifiers_and_properties.rs @@ -235,6 +235,7 @@ echo "end"; ); assert!(err.contains("uncaught"), "{err}"); } +/// Verifies a typed static property explicitly assigned zero remains readable. #[test] fn test_typed_static_property_initialized_to_zero_reads_normally() { let out = compile_and_run( @@ -412,7 +413,8 @@ fn test_example_final_classes_compiles_and_runs() { #[test] fn test_example_typed_properties_compiles_and_runs() { let out = compile_and_run(include_str!("../../../examples/typed-properties/main.php")); - assert_eq!(out, "Ada:42\nmissing email\n"); + let eol = target().platform.php_eol(); + assert_eq!(out, format!("Ada:42{eol}missing email{eol}")); } /// Verifies PHP 8.4 asymmetric visibility at runtime: a `public private(set)` property is diff --git a/tests/codegen/optimizer/dead_code_elimination/guards/composite_guards.rs b/tests/codegen/optimizer/dead_code_elimination/guards/composite_guards.rs index b3e45d6a57..af205e4d29 100644 --- a/tests/codegen/optimizer/dead_code_elimination/guards/composite_guards.rs +++ b/tests/codegen/optimizer/dead_code_elimination/guards/composite_guards.rs @@ -150,7 +150,7 @@ run(10); } /// Verifies that a nested elseif with `true` body is pruned when a composite guard makes it -/// unreachable. Confirms "acx" with "dead" absent. +/// unreachable. Confirms "acx" with "dead-composite" absent. #[test] fn test_dead_code_elimination_prunes_nested_elseif_from_composite_guard_refinement() { let dir = make_cli_test_dir("elephc_dead_code_elimination_composite_guard_refinement"); @@ -162,7 +162,7 @@ function run($a, $b, $c) { if ($a && $b) { echo "a"; } elseif (true) { - echo "dead"; + echo "dead-composite"; } } else { echo "c"; @@ -192,7 +192,11 @@ run(false, false, false); ); assert_eq!(out, "acx"); - assert!(!user_asm.contains("dead")); + // The marker is distinctive, like every other fixture here: a bare "dead" + // also matches incidental prose in the emitted assembly -- the main prologue + // comments its SIGPIPE handler with "a dead peer" -- so the assertion failed + // while the branch was in fact pruned. + assert!(!user_asm.contains("dead-composite")); } /// Verifies that a nested subexpr contradicting a composite guard is pruned. Confirms "acx" diff --git a/tests/codegen/optimizer/release_local_slot.rs b/tests/codegen/optimizer/release_local_slot.rs index 5728ca938c..42a7d3ca35 100644 --- a/tests/codegen/optimizer/release_local_slot.rs +++ b/tests/codegen/optimizer/release_local_slot.rs @@ -56,9 +56,7 @@ fn run_release_fixture(source: &str, ir_opt: bool) -> (String, String) { "fixture compilation failed: {}", String::from_utf8_lossy(&compile.stderr) ); - let output = Command::new(dir.join("main")) - .output() - .expect("failed to run ReleaseLocalSlot fixture"); + let output = run_binary(&dir.join("main"), &dir); assert!( output.status.success(), "fixture execution failed: {}", diff --git a/tests/codegen/runtime_gc.rs b/tests/codegen/runtime_gc.rs index de5163a0e7..dee92ecf5d 100644 --- a/tests/codegen/runtime_gc.rs +++ b/tests/codegen/runtime_gc.rs @@ -15,6 +15,8 @@ mod nullable_string_return; mod regressions; #[path = "runtime_gc/assoc_rebind_release.rs"] mod assoc_rebind_release; +#[path = "runtime_gc/oversized_read_release.rs"] +mod oversized_read_release; #[path = "runtime_gc/stack_args.rs"] mod stack_args; #[path = "runtime_gc/cow_and_cycles.rs"] diff --git a/tests/codegen/runtime_gc/heap.rs b/tests/codegen/runtime_gc/heap.rs index d09ff8637f..75f91af0a1 100644 --- a/tests/codegen/runtime_gc/heap.rs +++ b/tests/codegen/runtime_gc/heap.rs @@ -355,6 +355,19 @@ fn test_heap_debug_reports_exit_summary() { ); } +/// Verifies platform bootstrap allocations are released before heap-debug +/// diagnostics, including the UTF-8 argv copy required by Windows executables. +#[test] +fn test_heap_debug_empty_program_releases_process_bootstrap_storage() { + let out = compile_and_run_with_heap_debug("make(); echo $wad->name; "#, - path = path.display() + path = php_path ); let out = compile_and_run_with_heap_size(&source, 67_108_864); @@ -501,6 +526,7 @@ fn test_file_get_contents_owned_success_result_is_released_after_string_cast() { let id = TEST_ID.fetch_add(1, Ordering::SeqCst); let path = std::env::temp_dir().join(format!("elephc_fgc_owned_success_{}.txt", id)); fs::write(&path, b"payload").unwrap(); + let php_path = path.to_string_lossy().replace('\\', "/"); let source = format!( r#" bool { } } -// Injects an exit harness into user assembly before the final `ret` instruction. -// Rewrites macOS-style syscall sequence to Linux-style syscall sequence if needed, -// then patches the assembly in-place using a target-specific needle. Panics if the -// needle is not found (indicates a codegen emit change that broke the harness injection). -/// Injects main exit harness into the compiler metadata registry. +/// Injects a raw runtime harness immediately before the generated main exit sequence. +/// +/// The fixture harnesses use the backend's internal register convention. Their +/// syscall sequences are rewritten for the selected target before insertion; +/// Windows terminates through `__rt_sys_exit`, while Linux retains syscall 60. +/// Panics when the target-specific exit needle is absent so a bootstrap change +/// cannot silently turn ownership checks into passing no-ops. pub(crate) fn inject_main_exit_harness(asm: &str, harness: &str) -> String { - let needle = match (target().platform, target().arch) { + inject_main_exit_harness_for_target(target(), asm, harness) +} + +/// Inserts a fixture harness at the supplied target's generated terminal exit path. +/// +/// Windows has no fixed immediate exit-code instruction: the bootstrap preserves +/// the compiled return code in `rbx` before flushing output and calling +/// `__rt_sys_exit`. Selecting the final call keeps explicit PHP `exit()` paths +/// out of the harness insertion point. +fn inject_main_exit_harness_for_target(target: Target, asm: &str, harness: &str) -> String { + if matches!((target.platform, target.arch), (Platform::Windows, Arch::X86_64)) { + return inject_windows_main_exit_harness(asm, &target.transform_assembly(harness)); + } + + let needle = match (target.platform, target.arch) { (Platform::MacOS, Arch::AArch64) => " mov x0, #0\n mov x16, #1\n svc #0x80", (Platform::Linux, Arch::AArch64) => " mov x0, #0\n mov x8, #93\n svc #0", (Platform::Linux, Arch::X86_64) => " mov edi, 0\n mov eax, 60\n syscall", - (_, Arch::AArch64) => panic!( + (Platform::Windows, Arch::AArch64) => panic!( "main exit harness is not implemented yet for target {}", - target() + target ), - (_, Arch::X86_64) => panic!( + (Platform::MacOS, Arch::X86_64) => panic!( "main exit harness is not implemented yet for target {}", - target() + target ), + (Platform::Windows, Arch::X86_64) => unreachable!("handled above"), }; - // Harness strings are written in macOS assembly dialect; transform for Linux if needed - let harness = target().transform_assembly(harness); + // Harness strings use the shared backend dialect; rewrite target syscalls before insertion. + let harness = target.transform_assembly(harness); let replacement = format!("{harness}\n{needle}"); let patched = asm.replacen(needle, &replacement, 1); assert_ne!(patched, asm, "failed to inject main exit harness"); patched } +/// Inserts a fixture harness before the final Windows runtime-exit call while restoring +/// the already-staged SysV exit-code register that arbitrary harness calls may clobber. +fn inject_windows_main_exit_harness(asm: &str, harness: &str) -> String { + const WINDOWS_MAIN_EXIT_HOOK: &str = " call __rt_sys_exit"; + + let hook = asm + .rfind(WINDOWS_MAIN_EXIT_HOOK) + .expect("failed to locate Windows main exit hook"); + let harness = harness.trim_end_matches('\n'); + format!( + "{}{}\n mov rdi, rbx\n{}", + &asm[..hook], + harness, + &asm[hook..] + ) +} + // Compiles a PHP source snippet and runs it with an injected harness, expecting a failure. // Captures stderr from the resulting process and returns it for assertion. // Used for error-test fixtures that verify compile-time diagnostic messages. @@ -506,3 +547,48 @@ fn compile_and_run_with_repr(source: &str, null_repr: elephc::codegen::NullRepr) let _ = fs::remove_dir_all(&dir); elephc_out } + +#[cfg(test)] +mod tests { + //! Purpose: + //! Unit tests for target-specific codegen fixture assembly transformations. + //! + //! Called from: + //! - `cargo test --test codegen_tests` through Rust's test harness. + //! + //! Key details: + //! - Windows harness insertion must target the normal terminal exit rather + //! than an earlier explicit PHP `exit()` call. + + use super::*; + + /// Verifies Windows harness injection selects the final runtime exit hook and restores + /// the preserved process exit code after arbitrary fixture calls. + #[test] + fn inject_main_exit_harness_uses_final_windows_exit_hook() { + let target = Target { + platform: Platform::Windows, + arch: Arch::X86_64, + }; + let asm = concat!( + "__elephc_main:\n", + " call __rt_sys_exit\n", + " mov rdi, rbx\n", + " call __rt_sys_exit\n", + ); + + let patched = inject_main_exit_harness_for_target(target, asm, " call __rt_heap_check"); + + assert_eq!( + patched, + concat!( + "__elephc_main:\n", + " call __rt_sys_exit\n", + " mov rdi, rbx\n", + " call __rt_heap_check\n", + " mov rdi, rbx\n", + " call __rt_sys_exit\n", + ) + ); + } +} diff --git a/tests/codegen/support/platform.rs b/tests/codegen/support/platform.rs index 3d43f11206..06e01907b1 100644 --- a/tests/codegen/support/platform.rs +++ b/tests/codegen/support/platform.rs @@ -86,7 +86,9 @@ pub(crate) fn default_link_paths() -> Vec { } } Platform::Windows => { - panic!("Windows target is not yet supported (see issue #379)"); + // MinGW's `x86_64-w64-mingw32-gcc` resolves its own import libraries + // (kernel32, msvcrt, ...), so the windows-x86_64 measurement target + // needs no extra `-L` search paths threaded through here. } } // The elephc-tls / elephc-pdo bridge staticlib directory is added directly by @@ -136,11 +138,170 @@ pub(crate) fn qemu_sysroot() -> Option<&'static str> { None } Platform::MacOS => None, + // Windows binaries run under Wine, not qemu, so there is no sysroot. Platform::Windows => None, }) .as_deref() } +/// Reports whether the MinGW-w64 x86_64 cross toolchain is installed, by probing +/// `x86_64-w64-mingw32-gcc --version`. Required to assemble and link the +/// windows-x86_64 measurement target's `.exe`. Mirrors the probe used by the +/// dedicated `windows_pe` tests. +pub(crate) fn has_mingw() -> bool { + Command::new("x86_64-w64-mingw32-gcc") + .arg("--version") + .output() + .map(|o| o.status.success()) + .unwrap_or(false) +} + +/// Reports whether Wine is installed, by probing `wine64` first (the native 64-bit +/// loader) then falling back to `wine`. Required to execute a cross-compiled +/// windows-x86_64 `.exe`. Mirrors the probe used by the `windows_pe` tests. +pub(crate) fn has_wine() -> bool { + Command::new("wine64") + .arg("--version") + .output() + .map(|o| o.status.success()) + .unwrap_or(false) + || Command::new("wine") + .arg("--version") + .output() + .map(|o| o.status.success()) + .unwrap_or(false) +} + +/// Returns the preferred Wine binary name: `wine64` when present, else `wine`. +/// Both run PE32+ binaries on modern distros; `wine64` is tried first to match the +/// selection the `windows_pe` execution tests use. +pub(crate) fn wine_binary() -> &'static str { + if Command::new("wine64") + .arg("--version") + .output() + .map(|o| o.status.success()) + .unwrap_or(false) + { + "wine64" + } else { + "wine" + } +} + +/// Reports whether the windows-x86_64 test toolchain can build and execute PE files. +/// +/// MinGW-w64 is always required to assemble/link the GNU-target executable. +/// A native Windows host executes that PE directly; cross-host runs additionally +/// require Wine. The result is cached because every fixture probes this guard. +pub(crate) fn windows_toolchain_available() -> bool { + static WINDOWS_TOOLCHAIN_AVAILABLE: std::sync::OnceLock = std::sync::OnceLock::new(); + *WINDOWS_TOOLCHAIN_AVAILABLE.get_or_init(|| has_mingw() && (cfg!(windows) || has_wine())) +} + +/// Reports whether the current job has declared that windows-x86_64 fixtures must +/// really execute, through `ELEPHC_REQUIRE_WINDOWS_TOOLCHAIN`. +/// +/// `ensure_windows_runnable_or_skip` treats a missing toolchain as a graceful +/// skip that nextest records as a pass. That is right on a developer machine, +/// but it is the one way the strict native gate can go green while proving +/// nothing: `windows-codegen-gate` compares testcase *names* against the +/// runnable inventory, so a shard whose MinGW dropped off `PATH` would emit the +/// same complete, failure-free JUnit report as a shard that executed every +/// fixture. CI jobs that install the toolchain deliberately therefore set this +/// variable, which converts the skip into a hard failure. +pub(crate) fn windows_toolchain_is_required() -> bool { + windows_toolchain_requirement_from( + std::env::var("ELEPHC_REQUIRE_WINDOWS_TOOLCHAIN").ok().as_deref(), + ) +} + +/// Decides the toolchain requirement from a raw variable value. Split out as a +/// pure helper so the gating can be unit-tested without mutating process +/// environment, which is racy under parallel test execution. +fn windows_toolchain_requirement_from(value: Option<&str>) -> bool { + value.is_some_and(|value| value == "1" || value.eq_ignore_ascii_case("true")) +} + +/// Gracefully skips the current codegen fixture when it targets windows-x86_64 but +/// its host-appropriate execution toolchain is missing (e.g. a macOS dev host +/// without Wine, or a native Windows host without MinGW-w64). +/// +/// Exits the test process with success. Under `cargo nextest` each test runs in its +/// own process, so this reports the individual test as passed/skipped rather than +/// failing it — the same "guard and return" outcome the dedicated `windows_pe` +/// tests use, adapted to helpers that cannot early-return through the caller's +/// assertion. On every non-Windows target this is a no-op, so the native suite is +/// completely unaffected. +/// +/// When `ELEPHC_REQUIRE_WINDOWS_TOOLCHAIN` is set the skip is refused outright: +/// see `windows_toolchain_is_required`. +pub(crate) fn ensure_windows_runnable_or_skip() { + if target().platform != Platform::Windows { + return; + } + if !windows_toolchain_available() { + assert!( + !windows_toolchain_is_required(), + "windows-x86_64 execution toolchain is unavailable, but this job set \ + ELEPHC_REQUIRE_WINDOWS_TOOLCHAIN: it declared that every Windows fixture \ + really executes, so skipping here would report a vacuous pass to the \ + strict native codegen gate" + ); + eprintln!( + "skipping windows-x86_64 codegen fixture: MinGW-w64/native-or-Wine execution toolchain unavailable" + ); + std::process::exit(0); + } +} + +/// Reports whether a dedicated `windows_pe` fixture may proceed, given the parts +/// of the toolchain it needs: MinGW-w64 always, plus a way to execute the produced +/// PE (a native Windows host, else Wine) when `needs_execution` is set. +/// +/// Returns `true` to proceed and `false` to skip, and panics instead of returning +/// `false` when the job set `ELEPHC_REQUIRE_WINDOWS_TOOLCHAIN`. The `windows_pe` +/// module cannot use `ensure_windows_runnable_or_skip`: those tests run with the +/// host target selected (so `target().platform` is not Windows on the Ubuntu Wine +/// jobs) and they guard on MinGW and Wine separately. Without this, a job whose +/// MinGW or Wine install silently degraded would report every `windows_pe` test as +/// passed while compiling and executing nothing — the same vacuous-pass hole the +/// strict codegen gate closes, on the four jobs the gate does not cover +/// (`windows-pe-cross-compile`, `windows-pe-llvm-lld`, `windows-bridge-native-build`). +pub(crate) fn windows_pe_fixture_may_run(what: &str, needs_execution: bool) -> bool { + let mingw = has_mingw(); + let can_execute = !needs_execution || cfg!(windows) || has_wine(); + if mingw && can_execute { + return true; + } + let missing = if !mingw { + "MinGW-w64 (x86_64-w64-mingw32-gcc)" + } else { + "a PE execution host (native Windows or Wine)" + }; + assert!( + !windows_toolchain_is_required(), + "skipping {what}: {missing} is unavailable, but this job set \ + ELEPHC_REQUIRE_WINDOWS_TOOLCHAIN — it declared that Windows PE fixtures \ + really compile and run, so skipping here would report a vacuous pass" + ); + eprintln!("skipping {what}: {missing} not found"); + false +} + +/// Applies the target's final assembly rewrite before assembling. For +/// windows-x86_64 this rewrites the shared x86_64 backend's raw Linux syscall +/// sequences into `__rt_sys_*` shim calls, exactly as the CLI pipeline +/// (`src/pipeline.rs`) and runtime cache (`src/runtime_cache.rs`) do before +/// assembling. For every other target it returns the assembly unchanged, so the +/// native suite stays byte-identical. +pub(crate) fn finalize_asm_for_target(asm: &str) -> String { + if target().platform == Platform::Windows { + elephc::codegen::platform::transform_for_windows(asm) + } else { + asm.to_string() + } +} + /// Verifies `effective_link_libs` filters out "System" from the library list. /// The macOS linker handles "System" specially and does not accept it as a /// normal `-l` argument. Input fixture: ["System", "crypto"] → ["crypto"]. @@ -149,3 +310,19 @@ fn test_effective_link_libs_ignores_system() { let libs = vec!["System".to_string(), "crypto".to_string()]; assert_eq!(effective_link_libs(&libs), vec!["crypto"]); } + +/// Verifies the strict-gate opt-in accepts exactly the affirmative spellings and +/// stays off when the variable is absent, empty, or negative. An accidental +/// "always true" here would break every developer host without Wine; an +/// accidental "always false" would silently restore the vacuous-pass path the +/// native Windows gate depends on being closed. +#[test] +fn test_windows_toolchain_requirement_parsing() { + assert!(windows_toolchain_requirement_from(Some("1"))); + assert!(windows_toolchain_requirement_from(Some("true"))); + assert!(windows_toolchain_requirement_from(Some("TRUE"))); + assert!(!windows_toolchain_requirement_from(None)); + assert!(!windows_toolchain_requirement_from(Some(""))); + assert!(!windows_toolchain_requirement_from(Some("0"))); + assert!(!windows_toolchain_requirement_from(Some("false"))); +} diff --git a/tests/codegen/support/projects.rs b/tests/codegen/support/projects.rs index beb4200c88..e12ed9150f 100644 --- a/tests/codegen/support/projects.rs +++ b/tests/codegen/support/projects.rs @@ -89,6 +89,18 @@ pub(crate) fn elephc_cli_command(dir: &Path) -> Command { let mut cmd = Command::new(elephc_cli_bin()); cmd.env("XDG_CACHE_HOME", dir.join("cache-root")); cmd.current_dir(dir); + // When the codegen suite is measured against a cross-compilation target + // (`ELEPHC_TEST_TARGET`), tell the CLI to compile for that same target so + // CLI-subprocess fixtures stay consistent with the in-process helpers, which + // read the target from `target()`. The CLI parses `--target` last-wins, so a + // later explicit `--target` in a test's own args still overrides this; that + // keeps target-selection fixtures (e.g. windows_pe, cli.rs) working. When the + // env var is unset there is no flag, so native behavior is byte-identical. + if let Ok(value) = std::env::var("ELEPHC_TEST_TARGET") { + if !value.is_empty() { + cmd.arg("--target").arg(value); + } + } cmd } @@ -200,9 +212,16 @@ pub(crate) fn compile_expect_type_error(source: &str) -> String { let resolved = elephc::resolver::resolve(ast, &dir).expect("resolve failed"); let resolved = elephc::autoload::collect_aliases(resolved); let resolved = elephc::pdo_prelude::inject_if_used(resolved, false); - let resolved = elephc::name_resolver::resolve(resolved).expect("name resolve failed"); let resolved = - elephc::autoload::run(resolved, &dir, &autoload_registry).expect("autoload failed"); + elephc::name_resolver::resolve_for_platform(resolved, target().platform) + .expect("name resolve failed"); + let resolved = elephc::autoload::run_for_platform( + resolved, + &dir, + &autoload_registry, + target().platform, + ) + .expect("autoload failed"); let resolved = elephc::optimize::fold_constants(resolved); let error = match elephc::types::check_with_target(&resolved, target()) { Ok(_) => panic!("source unexpectedly passed type checking"), @@ -257,7 +276,9 @@ pub(crate) fn compile_and_run_files_expect_failure( let ast = elephc::conditional::apply(ast, &define_set); let resolved = elephc::resolver::resolve(ast, base_dir).expect("resolve failed"); let resolved = elephc::autoload::collect_aliases(resolved); - let resolved = elephc::name_resolver::resolve(resolved).expect("name resolve failed"); + let resolved = + elephc::name_resolver::resolve_for_platform(resolved, target().platform) + .expect("name resolve failed"); let resolved = elephc::optimize::fold_constants(resolved); let check_result = elephc::types::check_with_target(&resolved, target()).expect("type check failed"); @@ -329,9 +350,16 @@ pub(crate) fn compile_and_run_files_with_defines( elephc::codegen::set_autoload_rule_count(autoload_registry.rule_count()); let resolved = elephc::resolver::resolve(ast, base_dir).expect("resolve failed"); let resolved = elephc::autoload::collect_aliases(resolved); - let resolved = elephc::name_resolver::resolve(resolved).expect("name resolve failed"); let resolved = - elephc::autoload::run(resolved, base_dir, &autoload_registry).expect("autoload failed"); + elephc::name_resolver::resolve_for_platform(resolved, target().platform) + .expect("name resolve failed"); + let resolved = elephc::autoload::run_for_platform( + resolved, + base_dir, + &autoload_registry, + target().platform, + ) + .expect("autoload failed"); let resolved = elephc::optimize::fold_constants(resolved); let check_result = elephc::types::check_with_target(&resolved, target()).expect("type check failed"); @@ -412,7 +440,8 @@ pub(crate) fn compile_files_fails_with_defines( let ast = elephc::conditional::apply(ast, &define_set); let resolved = elephc::resolver::resolve(ast, base_dir)?; let resolved = elephc::autoload::collect_aliases(resolved); - let resolved = elephc::name_resolver::resolve(resolved)?; + let resolved = + elephc::name_resolver::resolve_for_platform(resolved, target().platform)?; let resolved = elephc::optimize::fold_constants(resolved); elephc::types::check_with_target(&resolved, target())?; Ok(()) @@ -427,6 +456,10 @@ pub(crate) fn compile_files_fails_with_defines( // Used for tests that verify runtime behavior with specific input (e.g., read(), fgets). /// Provides the Compile and run with stdin helper used by the projects module. pub(crate) fn compile_and_run_with_stdin(source: &str, stdin_data: &str) -> String { + // Skip early on the windows target when the MinGW/Wine toolchain is missing, + // before touching the assembler or Wine (this helper assembles/runs inline + // rather than through `assemble_from_stdin`/`run_binary`). + ensure_windows_runnable_or_skip(); let id = TEST_ID.fetch_add(1, Ordering::SeqCst); let tid = std::thread::current().id(); let pid = std::process::id(); @@ -439,7 +472,9 @@ pub(crate) fn compile_and_run_with_stdin(source: &str, stdin_data: &str) -> Stri let ast = elephc::magic_constants::substitute_file_and_scope_constants(ast, &synthetic_main); let resolved = elephc::resolver::resolve(ast, &dir).expect("resolve failed"); let resolved = elephc::autoload::collect_aliases(resolved); - let resolved = elephc::name_resolver::resolve(resolved).expect("name resolve failed"); + let resolved = + elephc::name_resolver::resolve_for_platform(resolved, target().platform) + .expect("name resolve failed"); let resolved = elephc::optimize::fold_constants(resolved); let check_result = elephc::types::check_with_target(&resolved, target()).expect("type check failed"); @@ -468,7 +503,16 @@ pub(crate) fn compile_and_run_with_stdin(source: &str, stdin_data: &str) -> Stri let obj_path = dir.join("test.o"); let bin_path = dir.join("test"); - fs::write(&asm_path, &user_asm).unwrap(); + // Apply the windows syscall→shim rewrite before assembling; a no-op on native + // targets, so the assembled bytes are unchanged there. + let windows_asm; + let user_asm = if target().platform == Platform::Windows { + windows_asm = finalize_asm_for_target(&user_asm); + windows_asm.as_str() + } else { + user_asm.as_str() + }; + fs::write(&asm_path, user_asm).unwrap(); let mut as_cmd = Command::new(assembler_cmd()); if target().platform == Platform::MacOS { @@ -488,19 +532,14 @@ pub(crate) fn compile_and_run_with_stdin(source: &str, stdin_data: &str) -> Stri ); use std::io::Write; - let bin_cmd = if target().platform == Platform::Linux - && target().arch == Arch::AArch64 - && cfg!(target_arch = "x86_64") - { - "qemu-aarch64-static" - } else { - bin_path.to_str().unwrap() - }; - let mut cmd = if target().platform == Platform::Linux + let mut cmd = if target().platform == Platform::Windows { + // Run the cross-compiled `.exe` under Wine, still piping stdin below. + build_run_command(&bin_path) + } else if target().platform == Platform::Linux && target().arch == Arch::AArch64 && cfg!(target_arch = "x86_64") { - let mut c = Command::new(bin_cmd); + let mut c = Command::new("qemu-aarch64-static"); c.arg(&bin_path); c } else { diff --git a/tests/codegen/support/runner.rs b/tests/codegen/support/runner.rs index b6c02b8506..68ee10981e 100644 --- a/tests/codegen/support/runner.rs +++ b/tests/codegen/support/runner.rs @@ -64,6 +64,17 @@ const DEFAULT_BINARY_TIMEOUT_SECS: u64 = 60; /// Assemble `asm` to `obj_path` by piping the source through `as`'s stdin so /// no intermediate `.s` file is created. fn assemble_from_stdin(asm: &str, obj_path: &Path) { + ensure_windows_runnable_or_skip(); + // Rewrite the shared x86_64 backend's raw Linux syscalls into windows shim + // calls before assembling; a no-op on native targets, so their bytes are + // unchanged (the borrowed `asm` is fed straight through). + let windows_asm; + let asm = if target().platform == Platform::Windows { + windows_asm = finalize_asm_for_target(asm); + windows_asm.as_str() + } else { + asm + }; let mut cmd = Command::new(assembler_cmd()); if target().platform == Platform::MacOS { cmd.args(["-arch", target().darwin_arch_name()]); @@ -105,6 +116,9 @@ pub(crate) fn get_runtime_obj() -> &'static Path { /// runtimes and custom heap sizes get distinct objects while repeated tests can /// still share the assembled output. pub(crate) fn runtime_obj_for_asm(runtime_asm: &str) -> std::path::PathBuf { + ensure_windows_runnable_or_skip(); + // Key the cache on the untransformed runtime assembly: the windows rewrite is + // deterministic, so identical raw assembly still shares one assembled object. let hash = runtime_asm_hash(runtime_asm); let cache = RUNTIME_OBJS_BY_ASM.get_or_init(|| Mutex::new(std::collections::HashMap::new())); let mut cache = cache.lock().expect("runtime asm cache poisoned"); @@ -116,6 +130,15 @@ pub(crate) fn runtime_obj_for_asm(runtime_asm: &str) -> std::path::PathBuf { fs::create_dir_all(&dir).unwrap(); let asm_path = dir.join(format!("runtime_{hash:016x}.s")); let obj_path = dir.join(format!("runtime_{hash:016x}.o")); + // Apply the windows syscall→shim rewrite before writing/assembling; a no-op on + // native targets, so the runtime bytes are unchanged there. + let windows_asm; + let runtime_asm = if target().platform == Platform::Windows { + windows_asm = finalize_asm_for_target(runtime_asm); + windows_asm.as_str() + } else { + runtime_asm + }; fs::write(&asm_path, runtime_asm).unwrap(); let mut cmd = Command::new(assembler_cmd()); @@ -161,20 +184,40 @@ fn requested_bridge_staticlibs<'a>(actual_link_libs: &[&str]) -> Vec<&'a TestBri .collect() } -/// Builds any requested bridge staticlibs missing from the debug target directory. +/// Builds any requested bridge staticlibs missing from the target's debug +/// directory. +/// +/// On the windows-x86_64 test target the bridge staticlibs must be cross-built +/// for `x86_64-pc-windows-gnu` (PE/COFF) so MinGW can link them into the `.exe`; +/// MinGW cannot link host ELF archives into a PE binary. To that end the +/// `cargo build -p ` command gains `--target x86_64-pc-windows-gnu` and +/// the `CC_x86_64_pc_windows_gnu`/`AR_x86_64_pc_windows_gnu`/`RANLIB_*` env vars +/// that cc-rs needs to compile bundled C (PDO's `libsqlite3-sys` amalgamation) +/// with the MinGW toolchain. On every other target the command is byte-identical +/// to the pre-Tier-2 path: no `--target`, no extra env, so macOS/Linux bridge +/// builds are unchanged. fn ensure_bridge_staticlibs(actual_link_libs: &[&str], bridge_staticlib_dir: &Path) { let _guard = BRIDGE_STATICLIB_BUILD_LOCK .get_or_init(|| Mutex::new(())) .lock() .expect("bridge staticlib build lock poisoned"); + let platform = target().platform; + let cargo_target = bridge_staticlib_cargo_target(platform); for bridge in requested_bridge_staticlibs(actual_link_libs) { let archive_path = bridge_staticlib_dir.join(format!("lib{}.a", bridge.lib_name)); if !bridge_staticlib_needs_build(&archive_path, bridge.package) { continue; } - let status = Command::new("cargo") - .args(["build", "-p", bridge.package]) + let mut cmd = Command::new("cargo"); + cmd.args(["build", "-p", bridge.package]); + if let Some(triple) = cargo_target { + cmd.args(["--target", triple]); + for (key, value) in bridge_staticlib_cross_env(platform) { + cmd.env(key, value); + } + } + let status = cmd .current_dir(env!("CARGO_MANIFEST_DIR")) .status() .unwrap_or_else(|err| { @@ -197,6 +240,50 @@ fn ensure_bridge_staticlibs(actual_link_libs: &[&str], bridge_staticlib_dir: &Pa } } +/// Returns the `cargo build --target` triple the bridge staticlib build should +/// target, or `None` when building for the host. Only the windows-x86_64 test +/// target cross-compiles the bridges; every other target returns `None` so the +/// `cargo build` command stays byte-identical to the pre-Tier-2 path. Split out +/// as a pure helper so the gating can be unit-tested without mutating process +/// environment (which is racy under parallel test execution). +fn bridge_staticlib_cargo_target(platform: Platform) -> Option<&'static str> { + match platform { + Platform::Windows => Some("x86_64-pc-windows-gnu"), + _ => None, + } +} + +/// Returns the `CC`/`AR`/`RANLIB` env vars cc-rs needs to compile bundled C +/// (PDO's `libsqlite3-sys` amalgamation) for the windows-x86_64 cross target +/// using the MinGW-w64 toolchain. Empty slice on non-Windows targets, so no env +/// is injected into the host `cargo build` command there. +fn bridge_staticlib_cross_env(platform: Platform) -> &'static [(&'static str, &'static str)] { + static WINDOWS: [(&str, &str); 3] = [ + ("CC_x86_64_pc_windows_gnu", "x86_64-w64-mingw32-gcc"), + ("AR_x86_64_pc_windows_gnu", "x86_64-w64-mingw32-ar"), + ("RANLIB_x86_64_pc_windows_gnu", "x86_64-w64-mingw32-ranlib"), + ]; + static OTHER: [(&str, &str); 0] = []; + match platform { + Platform::Windows => &WINDOWS, + _ => &OTHER, + } +} + +/// Returns the subdirectory under the cargo target dir where bridge staticlibs +/// land. For the windows-x86_64 cross target, `cargo build --target +/// x86_64-pc-windows-gnu` emits archives under +/// `/x86_64-pc-windows-gnu/debug`, so MinGW finds the PE/COFF archives +/// there; every other target uses `/debug` (the host cargo output dir), +/// preserving the pre-Tier-2 layout on macOS/Linux. Pure helper so the dir +/// resolution can be unit-tested without env mutation. +fn bridge_staticlib_subdir(platform: Platform) -> &'static str { + match platform { + Platform::Windows => "x86_64-pc-windows-gnu/debug", + _ => "debug", + } +} + /// Reports whether a bridge staticlib is missing or older than its package /// sources. This keeps codegen tests from linking stale bridge archives after a /// bridge crate changes inside the same worktree. Archived CI runs can declare @@ -368,7 +455,13 @@ pub(crate) fn link_binary( // shards derive it from their extracted executable, and local tests fall // back to the workspace target directory. let needs_bridge_staticlib = !requested_bridge_staticlibs(&actual_link_libs).is_empty(); - let bridge_staticlib_dir = bridge_staticlib_dir(); + let mut bridge_staticlib_dir = bridge_staticlib_dir(); + if target().platform == Platform::Windows + && !bridge_staticlib_dir.ends_with(bridge_staticlib_subdir(Platform::Windows)) + { + bridge_staticlib_dir.pop(); + bridge_staticlib_dir.push(bridge_staticlib_subdir(Platform::Windows)); + } if needs_bridge_staticlib { ensure_bridge_staticlibs(&actual_link_libs, &bridge_staticlib_dir); } @@ -451,32 +544,155 @@ pub(crate) fn link_binary( ); } Platform::Windows => { - panic!("Windows target is not yet supported (see issue #379)"); + // MinGW GCC (`x86_64-w64-mingw32-gcc`) links the user + runtime objects + // into a PE32+ `.exe`, mirroring the production windows arm in + // `src/linker.rs`. The `.exe` suffix matches what MinGW emits and what + // the Wine runner then executes. + let mut ld_cmd = Command::new(gcc_cmd()); + ld_cmd.arg("-o").arg(target_binary_path(bin_path)); + ld_cmd.arg(obj_path); + ld_cmd.arg(runtime_obj); + // Surface the CI MinGW sysroot (cross-built PCRE2/bzip2/zlib/iconv) + // before any `-l` args so MinGW resolves those C symbols. Mirrors the + // production arm in `src/linker.rs`; gated on the env var so local + // non-CI runs are unaffected. + for path in mingw_sysroot_link_paths() { + ld_cmd.arg(format!("-L{}", path)); + } + if needs_bridge_staticlib { + ld_cmd.arg(format!("-L{}", bridge_staticlib_dir.display())); + } + for path in extra_link_paths { + ld_cmd.arg(format!("-L{}", path)); + } + for lib in &actual_link_libs { + ld_cmd.arg(format!("-l{}", lib)); + } + if actual_link_libs + .iter() + .any(|library| *library == "elephc_magician") + { + // The Rust staticlib's Cargo native-link metadata does not reach + // this hand-built command. Repeat its C dependencies after the + // archive so GNU ld can resolve the symbols it introduces. + ld_cmd.args(windows_magician_transitive_libraries()); + } + // Windows system import libraries the runtime shims resolve against + // (WriteFile/ReadFile/HeapAlloc/BCryptGenRandom/...); same set as the + // production linker. secur32/userenv/ntdll cover the std-windows deps + // pulled in by pdo's postgres/mysql crates and by image's std usage + // (GetUserNameExW, GetUserProfileDirectoryW, Nt*File/RtlNtStatusToDosError). + // advapi32 resolves the ACL family behind the php-parity + // `is_readable()`/`is_writable()` check (`emit_win_acl_helpers`). + ld_cmd.args([ + "-lkernel32", + "-lmsvcrt", + "-lwinmm", + "-lws2_32", + "-ladvapi32", + "-lbcrypt", + "-lshlwapi", + "-lshell32", + "-lsecur32", + "-luserenv", + "-lntdll", + ]); + let ld_out = ld_cmd.output().expect("failed to run linker"); + assert!( + ld_out.status.success(), + "linker failed:\n{}", + String::from_utf8_lossy(&ld_out.stderr) + ); } } } -/// Runs a compiled binary directly, using qemu on Linux x86_64 to emulate ARM64. -/// On other platform/arch combinations, execs the binary natively. -/// Used for post-link execution of already-assembled test binaries. -pub(crate) fn run_binary(bin_path: &Path, dir: &Path) -> Output { - if target().platform == Platform::Linux - && target().arch == Arch::AArch64 - && cfg!(target_arch = "x86_64") - { - let mut cmd = Command::new("qemu-aarch64-static"); - if let Some(sysroot) = qemu_sysroot() { - cmd.args(["-L", sysroot]); - } - cmd.arg(bin_path).current_dir(dir); - run_command_with_timeout(cmd) +/// Returns MinGW libraries that must follow the eval bridge in harness links. +fn windows_magician_transitive_libraries() -> &'static [&'static str] { + &["-lpcre2-posix", "-lpcre2-8", "-liconv"] +} + +/// Returns the `-L` search paths derived from the `ELEPHC_MINGW_SYSROOT` env +/// var, mirroring `src/linker.rs::mingw_sysroot_link_paths` for the test +/// harness. CI sets the env var to a cross-built MinGW sysroot (PCRE2, bzip2, +/// zlib, libiconv) so the MinGW linker resolves the C symbols those codegen +/// fixtures call. Unset on local non-CI runs, so no missing-directory warnings. +fn mingw_sysroot_link_paths() -> Vec { + let Some(dir) = std::env::var_os("ELEPHC_MINGW_SYSROOT") else { + return Vec::new(); + }; + let base = std::path::PathBuf::from(dir); + if !base.is_dir() { + return Vec::new(); + } + let mut paths = Vec::new(); + let lib = base.join("lib"); + if lib.is_dir() { + paths.push(lib.to_string_lossy().into_owned()); + } + let lib64 = base.join("lib64"); + if lib64.is_dir() { + paths.push(lib64.to_string_lossy().into_owned()); + } + paths +} + +/// Returns the on-disk path of the compiled binary for the current target. For +/// windows-x86_64 this is `.exe` (MinGW emits a `.exe` and Wine runs it); +/// every other target uses the bare binary path unchanged. +pub(crate) fn target_binary_path(bin_path: &Path) -> std::path::PathBuf { + if target().platform == Platform::Windows { + bin_path.with_extension("exe") } else { - let mut cmd = Command::new(bin_path); - cmd.current_dir(dir); - run_command_with_timeout(cmd) + bin_path.to_path_buf() + } +} + +/// Applies host-side Wine diagnostics controls without altering the compiled +/// program's stdout or stderr streams. +fn configure_wine_command(cmd: &mut Command) { + cmd.env("WINEDEBUG", "-all"); + cmd.env("MVK_CONFIG_LOG_LEVEL", "0"); +} + +/// Builds the base `Command` that executes a compiled codegen fixture for the +/// current target: a direct exec on the host, `qemu-aarch64-static` when running +/// ARM64 binaries on an x86_64 host, or Wine running the `.exe` for the +/// windows-x86_64 target. Native Windows executes the PE directly; other hosts +/// use Wine. The caller sets the working directory and wires args/stdin/stdout +/// as needed. Centralizing dispatch keeps every runner path target-correct. +pub(crate) fn build_run_command(bin_path: &Path) -> Command { + match target().platform { + Platform::Windows if cfg!(windows) => Command::new(target_binary_path(bin_path)), + Platform::Windows => { + let mut cmd = Command::new(wine_binary()); + cmd.arg(target_binary_path(bin_path)); + configure_wine_command(&mut cmd); + cmd + } + Platform::Linux if target().arch == Arch::AArch64 && cfg!(target_arch = "x86_64") => { + let mut cmd = Command::new("qemu-aarch64-static"); + if let Some(sysroot) = qemu_sysroot() { + cmd.args(["-L", sysroot]); + } + cmd.arg(bin_path); + cmd + } + _ => Command::new(bin_path), } } +/// Runs a compiled binary for the current target, capturing stdout/stderr under a +/// timeout. Uses qemu to emulate ARM64 on an x86_64 host and Wine to run the `.exe` +/// on the windows-x86_64 target; execs natively otherwise. Used for post-link +/// execution of already-assembled test binaries. +pub(crate) fn run_binary(bin_path: &Path, dir: &Path) -> Output { + ensure_windows_runnable_or_skip(); + let mut cmd = build_run_command(bin_path); + cmd.current_dir(dir); + run_command_with_timeout(cmd) +} + /// Runs a child command with a timeout and captures stdout/stderr. fn run_command_with_timeout(mut cmd: Command) -> Output { let label = format!("{:?}", cmd); @@ -652,3 +868,78 @@ pub(crate) fn assemble_and_run_expect_failure( String::from_utf8(output.stderr).unwrap() } + +#[cfg(test)] +mod tests { + use super::*; + + /// Verifies Wine and MoltenVK host diagnostics are disabled at process + /// launch without filtering the compiled program's captured stderr. + #[test] + fn wine_command_disables_host_diagnostic_chatter() { + let mut cmd = Command::new("wine64"); + configure_wine_command(&mut cmd); + let env = cmd + .get_envs() + .map(|(key, value)| { + ( + key.to_string_lossy().into_owned(), + value.map(|value| value.to_string_lossy().into_owned()), + ) + }) + .collect::>(); + + assert_eq!(env.get("WINEDEBUG"), Some(&Some("-all".to_string()))); + assert_eq!( + env.get("MVK_CONFIG_LOG_LEVEL"), + Some(&Some("0".to_string())) + ); + } + + /// Verifies the windows-x86_64 target cross-compiles bridge staticlibs for + /// `x86_64-pc-windows-gnu` so MinGW can link the PE/COFF archives, while + /// macOS/Linux build for the host (no `--target`) and stay on the pre-Tier-2 + /// path. + #[test] + fn bridge_staticlib_cargo_target_gated_on_windows() { + assert_eq!( + bridge_staticlib_cargo_target(Platform::Windows), + Some("x86_64-pc-windows-gnu") + ); + assert_eq!(bridge_staticlib_cargo_target(Platform::MacOS), None); + assert_eq!(bridge_staticlib_cargo_target(Platform::Linux), None); + } + + /// Verifies the cc-rs `CC`/`AR`/`RANLIB` env vars are surfaced only for the + /// windows-x86_64 cross target, so PDO's bundled `libsqlite3-sys` amalgamation + /// is compiled by `x86_64-w64-mingw32-gcc` and not the host cc. Non-Windows + /// targets get an empty slice so the host `cargo build` command is unchanged. + #[test] + fn bridge_staticlib_cross_env_only_on_windows() { + let env = bridge_staticlib_cross_env(Platform::Windows); + assert_eq!(env.len(), 3); + assert!(env.iter().any(|(k, v)| *k == "CC_x86_64_pc_windows_gnu" + && *v == "x86_64-w64-mingw32-gcc")); + assert!(env.iter().any(|(k, v)| *k == "AR_x86_64_pc_windows_gnu" + && *v == "x86_64-w64-mingw32-ar")); + assert!(env.iter().any(|(k, v)| *k == "RANLIB_x86_64_pc_windows_gnu" + && *v == "x86_64-w64-mingw32-ranlib")); + assert!(bridge_staticlib_cross_env(Platform::MacOS).is_empty()); + assert!(bridge_staticlib_cross_env(Platform::Linux).is_empty()); + } + + /// Verifies bridge staticlibs are looked up under + /// `x86_64-pc-windows-gnu/debug` for the windows-x86_64 cross target (where + /// `cargo build --target x86_64-pc-windows-gnu` emits archives) and under + /// `debug` for every other target, preserving the pre-Tier-2 host layout on + /// macOS/Linux. + #[test] + fn bridge_staticlib_subdir_gated_on_windows() { + assert_eq!( + bridge_staticlib_subdir(Platform::Windows), + "x86_64-pc-windows-gnu/debug" + ); + assert_eq!(bridge_staticlib_subdir(Platform::MacOS), "debug"); + assert_eq!(bridge_staticlib_subdir(Platform::Linux), "debug"); + } +} diff --git a/tests/codegen/types/enums.rs b/tests/codegen/types/enums.rs index 766d7c1c7a..e34be45ce7 100644 --- a/tests/codegen/types/enums.rs +++ b/tests/codegen/types/enums.rs @@ -41,7 +41,8 @@ fn test_backed_enum_value_and_from_identity() { echo $c === Color::Green; ", ); - assert_eq!(out, "1\n1"); + let eol = target().platform.php_eol(); + assert_eq!(out, format!("1{eol}1")); } /// Regression: an enum used as a class property / promoted-constructor-param TYPE @@ -175,7 +176,8 @@ fn test_enum_try_from_and_cases() { echo $cases[1] === Color::Green; ", ); - assert_eq!(out, "1\n2\n1"); + let eol = target().platform.php_eol(); + assert_eq!(out, format!("1{eol}2{eol}1")); } /// Verifies string-backed enum: `Status::from("live")` resolves to `Status::Live` by identity, @@ -193,7 +195,8 @@ fn test_string_backed_enum_from_and_value() { echo Status::Live->value; ", ); - assert_eq!(out, "1\nlive"); + let eol = target().platform.php_eol(); + assert_eq!(out, format!("1{eol}live")); } /// Verifies pure (unit) enum: `Suit::cases()` returns all cases and `Suit::Hearts === $cases[0]` by identity. @@ -211,7 +214,8 @@ fn test_pure_enum_cases_identity() { echo $cases[0] === Suit::Hearts; ", ); - assert_eq!(out, "2\n1"); + let eol = target().platform.php_eol(); + assert_eq!(out, format!("2{eol}1")); } /// Verifies enum case objects expose PHP's readonly `name` property directly and inside methods. @@ -337,9 +341,12 @@ fn test_enum_from_string_failure_throws_value_error() { #[test] fn test_example_enums_compiles_and_runs() { let out = compile_and_run(include_str!("../../../examples/enums/main.php")); + let eol = target().platform.php_eol(); assert_eq!( out, - "1\n2\n3\nRed=1 Green=2 Blue=3 \nDefault=default Match=match MATCH=upper-match \nDESC" + format!( + "1{eol}2{eol}3{eol}Red=1 Green=2 Blue=3 {eol}Default=default Match=match MATCH=upper-match {eol}DESC" + ) ); } @@ -630,7 +637,8 @@ fn test_enum_method_reads_this_name() { echo Suit::Spades->describe(); ", ); - assert_eq!(out, "Hearts=h\nSpades=s"); + let eol = target().platform.php_eol(); + assert_eq!(out, format!("Hearts=h{eol}Spades=s")); } /// Verifies that an enum method can reference a class constant via `self::`. @@ -685,7 +693,8 @@ fn test_backed_enum_name_and_value() { echo Code::Err->value; ", ); - assert_eq!(out, "Err\n2"); + let eol = target().platform.php_eol(); + assert_eq!(out, format!("Err{eol}2")); } /// Verifies a string-backed enum case `->name` returns the case identifier, not the @@ -703,7 +712,8 @@ fn test_string_backed_enum_name_distinct_from_value() { echo Status::Live->value; ", ); - assert_eq!(out, "Live\nlive"); + let eol = target().platform.php_eol(); + assert_eq!(out, format!("Live{eol}live")); } /// Verifies `->name` reads correctly when the case singleton is aliased through a local @@ -722,7 +732,8 @@ fn test_enum_name_through_variable_and_cases() { echo $cases[1]->name; ", ); - assert_eq!(out, "Clubs\nHeartsClubs"); + let eol = target().platform.php_eol(); + assert_eq!(out, format!("Clubs{eol}HeartsClubs")); } /// Verifies `->name` works inside string interpolation alongside `->value`, matching PHP's diff --git a/tests/codegen/types/never.rs b/tests/codegen/types/never.rs index 853f3e91ee..cebcf13e51 100644 --- a/tests/codegen/types/never.rs +++ b/tests/codegen/types/never.rs @@ -193,5 +193,10 @@ fn test_never_overrides_void_parent() { #[test] fn test_example_never_compiles_and_runs() { let out = compile_and_run(include_str!("../../../examples/never/main.php")); - assert_eq!(out, "port = 8080\ncaught: config error: workers must be positive, got 0\n"); + let expected = if target().platform == Platform::Windows { + "port = 8080\r\ncaught: config error: workers must be positive, got 0\r\n" + } else { + "port = 8080\ncaught: config error: workers must be positive, got 0\n" + }; + assert_eq!(out, expected); } diff --git a/tests/codegen/windows_pe.rs b/tests/codegen/windows_pe.rs new file mode 100644 index 0000000000..c459ae2359 --- /dev/null +++ b/tests/codegen/windows_pe.rs @@ -0,0 +1,1601 @@ +//! Purpose: +//! Integration tests verifying that the Windows x86_64 cross-compilation target +//! produces valid, runnable PE32+ executables. Compile-only tests require the +//! MinGW-w64 toolchain (`x86_64-w64-mingw32-as`, `x86_64-w64-mingw32-gcc`) and are +//! skipped when it is not available. Execution tests run directly on a Windows +//! host and otherwise require Wine (`wine64` or `wine`). +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Compile-only tests assemble + link and validate PE32+ output via `file`. +//! - Execution tests run the produced `.exe` natively or under Wine and assert exact stdout, +//! which is the only signal that catches syscall/ABI regressions — compile-only +//! checks cannot detect those. +//! - Uses the CLI directly with `--target windows-x86_64`. +//! - Every skip goes through `windows_pe_fixture_may_run`, so a CI job that sets +//! `ELEPHC_REQUIRE_WINDOWS_TOOLCHAIN=1` fails loudly instead of reporting a +//! vacuous pass when its MinGW or Wine install degraded. + +use crate::support::*; +use std::io::{Read, Write}; +use std::net::{TcpListener, TcpStream}; +use std::process::Stdio; +use std::time::{Duration, Instant}; + +// `has_mingw`, `has_wine`, and `wine_binary` are shared with the parameterized +// codegen harness and live in `crate::support` (imported via the glob above), so +// there is a single source of truth for MinGW/Wine detection. + +/// Compiles a PHP source string to a Windows PE32+ binary and verifies the output. +/// Skips the test if MinGW-w64 is not installed, unless the job requires the +/// toolchain (see `windows_pe_fixture_may_run`), in which case it fails instead. +fn compile_windows_pe(source: &str) { + if !windows_pe_fixture_may_run("Windows PE test", false) { + return; + } + let id = TEST_ID.fetch_add(1, Ordering::SeqCst); + let dir = std::env::temp_dir().join(format!("elephc_win_test_{}_{}", std::process::id(), id)); + fs::create_dir_all(&dir).expect("create temp dir"); + let php_path = dir.join("test.php"); + fs::write(&php_path, source).expect("write php source"); + + let output = elephc_cli_command(&dir) + .arg("--target") + .arg("windows-x86_64") + .arg(&php_path) + .output() + .expect("run elephc"); + + assert!( + output.status.success(), + "elephc failed to compile for windows-x86_64:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + + let exe_path = dir.join("test.exe"); + assert!( + exe_path.exists(), + "expected output binary '{}' does not exist", + exe_path.display() + ); + + let file_output = Command::new("file") + .arg(&exe_path) + .output() + .expect("run file"); + + let file_str = String::from_utf8_lossy(&file_output.stdout).to_string(); + let _ = fs::remove_dir_all(&dir); + assert!( + file_str.contains("PE32+"), + "expected PE32+ executable, got: {}", + file_str + ); + assert!( + file_str.contains("x86-64"), + "expected x86-64 architecture, got: {}", + file_str + ); +} + +/// Verifies `--web` cross-builds its Rust bridge, links a PE executable without +/// force-loading duplicate Windows import objects, and retains the generated-to- +/// bridge ABI exports used by request and response helpers. +#[test] +fn test_windows_web_bridge_compiles_and_exports() { + if !windows_pe_fixture_may_run("Windows web bridge test", false) { + return; + } + let id = TEST_ID.fetch_add(1, Ordering::SeqCst); + let dir = std::env::temp_dir().join(format!( + "elephc_win_web_{}_{}", + std::process::id(), + id + )); + fs::create_dir_all(&dir).expect("create Windows web temp dir"); + let php_path = dir.join("test.php"); + fs::write( + &php_path, + " String { + let deadline = Instant::now() + Duration::from_secs(15); + let mut stream = loop { + match TcpStream::connect(("127.0.0.1", port)) { + Ok(stream) => break stream, + Err(error) if Instant::now() < deadline => { + let _ = error; + std::thread::sleep(Duration::from_millis(50)); + } + Err(error) => panic!("Windows web PE did not listen: {error}"), + } + }; + write!( + stream, + "GET {path} HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n" + ) + .expect("write Windows web request"); + let mut response = String::new(); + stream + .read_to_string(&mut response) + .expect("read Windows web response"); + response + }; + + let ok = request("/?name=ada"); + assert!(ok.starts_with("HTTP/1.1 200"), "{ok}"); + assert!(ok.to_ascii_lowercase().contains("x-powered-by: elephc"), "{ok}"); + assert!(ok.ends_with("Hello, ada!\n"), "{ok}"); + let missing = request("/"); + assert!(missing.starts_with("HTTP/1.1 400"), "{missing}"); + assert!(missing.ends_with("missing 'name' query parameter\n"), "{missing}"); + + let deadline = Instant::now() + Duration::from_secs(10); + let exit = loop { + if let Some(status) = child.try_wait().expect("poll Windows web PE") { + break status; + } + if Instant::now() >= deadline { + let _ = child.kill(); + panic!("Windows web PE did not stop after --max-requests 2"); + } + std::thread::sleep(Duration::from_millis(50)); + }; + assert!(exit.success(), "Windows web PE exited with {exit}"); + assert!(TcpStream::connect(("127.0.0.1", port)).is_err()); + } + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies PE loader mitigations and generated-frame unwind records using the +/// MinGW inspection tools, without executing the resulting binary. +#[test] +fn test_windows_pe_hardening_and_generated_unwind_metadata() { + if !windows_pe_fixture_may_run("Windows PE hardening test", false) { + return; + } + let id = TEST_ID.fetch_add(1, Ordering::SeqCst); + let dir = std::env::temp_dir().join(format!( + "elephc_win_hardening_{}_{}", + std::process::id(), + id + )); + fs::create_dir_all(&dir).expect("create hardening temp dir"); + let php_path = dir.join("test.php"); + fs::write( + &php_path, + "getMessage(); +} +"#, + ); +} + +/// Verifies that the manual Fiber stack switch, including its Windows x64 TEB +/// metadata resynchronization, assembles and links into a PE32+ executable. +#[test] +fn test_windows_fiber_suspend_resume_compiles_to_pe() { + compile_windows_pe( + r#"start(); +$fiber->resume("done"); +"#, + ); +} + +/// Verifies the Windows socketpair rejection path assembles and links. +#[test] +fn test_windows_stream_socket_pair_compiles_to_pe() { + compile_windows_pe( + r#"label = $label; + } + public function select(int $a, int $b, int $c, string $label, mixed $value): mixed { + if ($a > 0) { return $label; } + return $value; + } +} +function overflow_owned(int $a, int $b, int $c, int $d, string $label, StackOwned $object, mixed $value): mixed { + if ($a > 0) { return $label; } + if ($b > 0) { return $object; } + return $value; +} +function overflow_float(float $a, float $b, float $c, float $d, float $e): float { + return $a + $b + $c + $d + $e; +} +$object = new StackOwned(1, 2, 3, "ctor"); +$callable = overflow_owned(...); +$callable(1, 2, 3, 4, "callable", $object, ["mixed"]); +overflow_owned(0, 1, 2, 3, "direct", $object, ["mixed"]); +$object->select(1, 2, 3, "method", ["mixed"]); +overflow_float(1.0, 2.0, 3.0, 4.0, 5.0); +"#, + ); +} + +/// Verifies a native MSx64 extern callback trampoline, including descriptor +/// dispatch through internal helpers, assembles and links into a PE executable. +#[test] +fn test_windows_ffi_descriptor_callback_compiles_to_pe() { + compile_windows_pe( + r#"items[$offset]); } + public function offsetGet(mixed $offset): mixed { return $this->items[$offset]; } + public function offsetSet(mixed $offset, mixed $value): void { $this->items[$offset] = $value; } + public function offsetUnset(mixed $offset): void { $this->items = []; } + public function keep(int $value): bool { return $value <= $this->limit; } + public function overflow($a, $b, $c, $d, $e, $f, string $tail): string { return $tail; } + public function __destruct() { echo count($this->items); } +} +class WindowsStaticFilter { + public static int $limit = 2; + public static function keep(int $value): bool { return $value <= static::$limit; } +} + +$box = new WindowsSplBox(); +$box["nested"] = [10, 11]; +$alias = $box["nested"]; +$filtered = array_filter([1, 2, 3], $box->keep(...)); +$staticFiltered = array_filter([1, 2, 3], WindowsStaticFilter::keep(...)); +echo $box->overflow(1, 2, 3, 4, 5, 6, "stack"); +unset($box); +echo $alias[1], count($filtered), count($staticFiltered); +"#, + "stack11122", + ); +} + +/// Verifies destructor dispatch, heap-backed properties, overwrite cleanup, +/// and early-return cleanup execute without losing or duplicating releases. +#[test] +fn test_windows_destructor_and_early_exit_ownership_runs() { + compile_and_run_windows( + r#"tag = $tag; + $this->items = ["a", "b", "c"]; + } + public function __destruct() { + echo $this->tag . count($this->items); + } +} +function replace_and_return(bool $early): mixed { + $value = new WindowsOwnedDestructor("first"); + $value = new WindowsOwnedDestructor("second"); + if ($early) { return $value; } + return "done"; +} +replace_and_return(true); +replace_and_return(false); +"#, + "first3second3first3second3", + ); +} + +/// Compiles a PHP source string to a Windows PE32+ binary, executes it natively +/// on Windows or through Wine elsewhere, and asserts stdout plus exit status. +/// This is the primary regression net for Windows syscall/ABI codegen bugs, +/// since compile-only tests never run the produced binary. +fn compile_and_run_windows_expect_code(source: &str, expected_stdout: &str, expected_code: i32) { + compile_and_run_windows_expect_code_with_helpers(source, expected_stdout, expected_code, &[]); +} + +/// Compiles optional named PE helpers beside the main Windows fixture before +/// executing it, so process tests can exercise direct argv and environment +/// transport without introducing shell parsing into their acceptance signal. +fn compile_and_run_windows_expect_code_with_helpers( + source: &str, + expected_stdout: &str, + expected_code: i32, + helpers: &[(&str, &str)], +) { + if !windows_pe_fixture_may_run("Windows PE execution test", false) { + return; + } + let id = TEST_ID.fetch_add(1, Ordering::SeqCst); + let dir = std::env::temp_dir().join(format!("elephc_win_run_{}_{}", std::process::id(), id)); + fs::create_dir_all(&dir).expect("create temp dir"); + let mut expanded_source = source.to_string(); + for (name, helper_source) in helpers { + let helper_path = dir.join(format!("{name}.php")); + fs::write(&helper_path, helper_source).expect("write Windows PE helper source"); + let helper_output = elephc_cli_command(&dir) + .arg("--target") + .arg("windows-x86_64") + .arg(&helper_path) + .output() + .expect("compile Windows PE helper"); + assert!( + helper_output.status.success(), + "elephc failed to compile Windows PE helper '{name}':\n{}", + String::from_utf8_lossy(&helper_output.stderr) + ); + assert!( + dir.join(format!("{name}.exe")).exists(), + "expected Windows PE helper '{name}.exe' was not produced" + ); + let helper_exe = dir + .join(format!("{name}.exe")) + .canonicalize() + .expect("canonicalize Windows PE helper path"); + let helper_windows_path = if cfg!(windows) { + helper_exe.to_string_lossy().replace('\\', "/") + } else { + format!("Z:{}", helper_exe.to_string_lossy()) + }; + let marker = format!( + "__WINDOWS_HELPER_{}__", + name.to_ascii_uppercase().replace('-', "_") + ); + expanded_source = expanded_source.replace(&marker, &helper_windows_path); + } + let php_path = dir.join("test.php"); + fs::write(&php_path, expanded_source).expect("write php source"); + + let output = elephc_cli_command(&dir) + .arg("--target") + .arg("windows-x86_64") + .arg(&php_path) + .output() + .expect("run elephc"); + + assert!( + output.status.success(), + "elephc failed to compile for windows-x86_64:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + + let exe_path = dir.join("test.exe"); + assert!( + exe_path.exists(), + "expected output binary '{}' does not exist", + exe_path.display() + ); + + if !windows_pe_fixture_may_run("Windows PE execution (binary compiled successfully)", true) { + let _ = fs::remove_dir_all(&dir); + return; + } + + let (run_result, runner_label) = if cfg!(windows) { + ( + Command::new(&exe_path).current_dir(&dir).output(), + "native Windows", + ) + } else { + let wine_bin = wine_binary(); + ( + Command::new(wine_bin) + .arg(&exe_path) + .env("WINEDEBUG", "-all") + .current_dir(&dir) + .output(), + wine_bin, + ) + }; + + let run_output = match run_result { + Ok(o) => o, + Err(e) => { + let _ = fs::remove_dir_all(&dir); + panic!( + "failed to execute '{}' with {}: {}", + exe_path.display(), + runner_label, + e + ); + } + }; + + let stdout = String::from_utf8_lossy(&run_output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&run_output.stderr).to_string(); + let actual_code = run_output.status.code(); + let _ = fs::remove_dir_all(&dir); + + assert_eq!( + actual_code, + Some(expected_code), + "windows binary exited with code {:?}, expected {} with {}\nstdout: {:?}\nstderr: {:?}", + actual_code, + expected_code, + runner_label, + stdout, + stderr + ); + + // The elephc `echo` runtime path does not append a trailing newline, but + // tolerate one here in case Wine's console emulation adds one, so this + // helper stays robust to that Wine-specific detail rather than the compiler's. + let normalized = stdout.strip_suffix('\n').unwrap_or(&stdout); + assert_eq!( + normalized, expected_stdout, + "unexpected stdout from windows binary with {}\nfull stdout: {:?}\nstderr: {:?}", + runner_label, stdout, stderr + ); +} + +/// Compiles a PHP source string to a Windows PE32+ binary, runs it natively or +/// through Wine, and asserts stdout plus a successful exit status. +fn compile_and_run_windows(source: &str, expected_stdout: &str) { + compile_and_run_windows_expect_code(source, expected_stdout, 0); +} + +/// Verifies an exception crosses a generated frame and reaches its Windows catch handler. +#[test] +fn test_windows_cross_function_exception_unwinds_at_runtime() { + compile_and_run_windows( + r#"getMessage(); +} +"#, + "boom", + ); +} + +/// Verifies a complete Windows Fiber start/suspend/resume cycle while the fiber +/// stack performs runtime heap allocation and a CRT-backed numeric formatting call. +#[test] +fn test_windows_fiber_teb_stack_metadata_runs() { + compile_and_run_windows( + r#"start(), "|"; +$fiber->resume("resume"); +"#, + "128:1234.50|resume|done", + ); +} + +/// Verifies that `echo 'hello'` produces exactly "hello" on stdout when the +/// cross-compiled Windows binary is executed under Wine, proving the +/// WriteFile-based echo syscall shim works end-to-end on the real ABI. +#[test] +fn test_windows_run_echo_hello() { + compile_and_run_windows("= 80400` — the canonical feature-detection +/// idiom — evaluates true when run natively on Windows under Wine, proving the +/// emulated-PHP version constants resolve identically across targets. +#[test] +fn test_windows_run_php_version_id_feature_detection() { + compile_and_run_windows( + "= 80400 ? 'yes' : 'no';", + "yes", + ); +} + +/// Verifies that string concatenation produces correct output when run +/// natively on Windows under Wine. +#[test] +fn test_windows_run_string_concat() { + compile_and_run_windows("()\\\""), ":"; +echo escapeshellcmd("a%!^&|<>()\\\""); +"#, + ); +} + +/// Executes PHP's Windows-specific cmd.exe escaping rules under the native Windows CI runner. +#[test] +fn test_windows_shell_escaping_run() { + compile_and_run_windows( + r#"()'), ':'; +echo escapeshellcmd('a%!^&|<>()'); +"#, + "\"a ^&|<>()\":a^%^!^^^&^|^<^>^(^)", + ); +} + +/// Executes php-src's Windows backslash rules: internal runs pass through and only an odd final run doubles. +#[test] +fn test_windows_shell_escaping_backslash_runs_run() { + compile_and_run_windows( + r#"getMessage(), '|'; } +try { escapeshellcmd(str_repeat('a', 8190)); } catch (\ValueError $e) { echo $e->getMessage(), '|'; } +try { escapeshellarg(str_repeat('%', 8189)); } catch (\ValueError $e) { echo $e->getMessage(), '|'; } +try { escapeshellcmd(str_repeat('&', 8189)); } catch (\ValueError $e) { echo $e->getMessage(); } +"#, + "Argument exceeds the allowed length of 8192 bytes|Command exceeds the allowed length of 8192 bytes|Escaped command exceeds the allowed length of 8192 bytes", + ); +} + +/// Verifies proc_open/proc_close compile for the Windows PE target (C1c: real +/// `CreatePipe`/`CreateProcessW`, not a stub). Compile-only because there is no +/// Wine available in this harness; this catches link failures against the +/// `CreatePipe`/`CreateProcessW`/`WaitForSingleObject`/`GetExitCodeProcess`/ +/// `SetHandleInformation`/`SetErrorMode` Win32 imports pulled in by +/// `__rt_proc_open`/`__rt_proc_close`, and the kind-5 mixed-free destructor +/// arm. Runtime behavior (actually spawning `cmd.exe` and reading the pipes) +/// is exercised by CI under Wine. +#[test] +fn test_windows_proc_open_close_compile() { + compile_windows_pe(r#" ["pipe", "rb"], 1 => ["pipe", "wb"]], $pipes); +proc_close($r); +"#); +} + +/// Verifies the Windows PE runtime compiles an integer pipe mode through the +/// PHP scalar-coercion/read-direction path rather than rejecting the descriptor. +#[test] +fn test_windows_proc_open_integer_pipe_mode_compile() { + compile_windows_pe(r#" ["pipe", 1], 1 => ["pipe", "w"]], $pipes); +if ($r !== false) { proc_close($r); } +"#); +} + +/// Verifies a sparse integer descriptor spec compiles through the Windows PE +/// backend, exercising the keyed `$pipes` publication/writeback ABI. +#[test] +fn test_windows_proc_open_sparse_descriptor_keys_compile() { + compile_windows_pe( + r#" ['pipe', 'w']], $pipes); +echo count($pipes); +if ($process !== false) { proc_close($process); } +"#, + ); +} + +/// Compiles Windows-native non-pipe descriptor sources. This covers the +/// `CreateFileW` strict-path route, child-handle duplication for redirects and +/// supplied stream resources, and explicit `null` descriptors without +/// publishing any of those child-only handles into `$pipes`. +#[test] +fn test_windows_proc_open_child_only_descriptors_compile() { + compile_windows_pe( + r#" ['null'], + 1 => ['file', 'proc-open-output.txt', 'ab'], + 2 => ['redirect', 1], + 3 => $source, + ], + $pipes, +); +if ($process !== false) { proc_close($process); } +fclose($source); +"#, + ); +} + +/// Compiles the Windows-only `proc_open(["socket"])` descriptor path. The +/// endpoint is deliberately a raw Winsock resource rather than a CRT fd, so +/// this catches missing imports and accidental `_open_osfhandle` adoption. +#[test] +fn test_windows_proc_open_socket_descriptor_compile() { + compile_windows_pe( + r#" ['socket'], 1 => ['pipe', 'w']], + $pipes, +); +if ($process !== false) { + stream_filter_append($pipes[0], 'string.rot13'); + fwrite($pipes[0], "socket-ok\r\n"); + fclose($pipes[0]); + stream_set_blocking($pipes[1], true); + echo fread($pipes[1], 64); + fclose($pipes[1]); + echo ':' . proc_close($process); +} +"#, + ); +} + +/// Runs a filtered child exchange across the private loopback socket pair when +/// a Windows host or Wine is available. This verifies the bounded Windows +/// stream slot lets raw Winsock resources use the same filter semantics as PHP +/// streams without indexing tables by an opaque `SOCKET`. A compiled helper +/// echoes stdin byte-for-byte so shell utilities cannot normalize line endings. +#[test] +fn test_windows_proc_open_socket_descriptor_runs() { + compile_and_run_windows_expect_code_with_helpers( + r#" true]; +$process = proc_open( + ['__WINDOWS_HELPER_PROC_SOCKET_HELPER__'], + [0 => ['socket'], 1 => ['pipe', 'w']], + $pipes, + null, + null, + $options, +); +if ($process === false) { echo 'spawn-failed'; exit(1); } +if (stream_filter_append($pipes[0], 'string.rot13') === false) { echo 'filter-failed:'; } +fwrite($pipes[0], "socket-ok\r\n"); +fclose($pipes[0]); +stream_set_blocking($pipes[1], true); +echo fread($pipes[1], 64); +fclose($pipes[1]); +echo ':' . proc_close($process); +"#, + "fbpxrg-bx\r\n:0", + 0, + &[("proc-socket-helper", " ["pipe", "r"], 1 => ["pipe", "w"], 2 => ["pipe", "w"]], + $pipes +); +proc_close($r); +"#); +} + +/// Compiles a quoting/Unicode corpus through the Windows Wide proc_open path: +/// non-ASCII text, nested quotes, shell metacharacters, percent expansion, and +/// trailing backslashes must all survive PHP parsing and PE assembly/linking. +#[test] +fn test_windows_proc_open_unicode_and_quoting_corpus_compile() { + compile_windows_pe(r#" ['pipe', 'w']], $pipes); proc_close($a); +$b = proc_open('echo "quoted value" ^& echo second', [1 => ['pipe', 'w']], $pipes); proc_close($b); +$c = proc_open('echo %PATH% ^| findstr Windows', [1 => ['pipe', 'w']], $pipes); proc_close($c); +$d = proc_open('echo C:\\Temp\\', [1 => ['pipe', 'w']], $pipes); proc_close($d); +"#); +} + +/// Verifies a non-ASCII Windows working directory is transported through the +/// extended EIR/runtime ABI and strictly converted for CreateProcessW. +#[test] +fn test_windows_proc_open_unicode_cwd_compile() { + compile_windows_pe(r#" ['pipe', 'w']], $pipes, 'C:\\Données 日本語'); +proc_close($p); +"#); +} + +/// Verifies array commands, a Unicode/scalar environment, and bypass_shell are +/// lowered into the direct CreateProcessW ABI and link as a PE32+ executable. +#[test] +fn test_windows_proc_open_advanced_marshalling_compile() { + compile_windows_pe(r#" ['pipe', 'w']], + $pipes, + 'C:\\Données 日本語', + ['ELEPHC_TEXT' => 'été 日本語', 'EMPTY' => '', 'COUNT' => 42, 'ENABLED' => true], + ['bypass_shell' => true] +); +proc_close($a); +"#); +} + +/// Verifies reordered PHP named arguments retain source evaluation order while +/// the hidden Windows marshalling operands use canonical proc_open parameters. +#[test] +fn test_windows_proc_open_named_advanced_marshalling_compile() { + compile_windows_pe(r#" true], + env_vars: ['LANG' => 'fr_FR.UTF-8', 'MESSAGE' => 'café 日本語'], + cwd: 'C:\\Données', + pipes: $pipes, + descriptor_spec: [1 => ['pipe', 'w']], + command: ['C:\\PHP\\php.exe', '-v'] +); +proc_close($process); +"#); +} + +/// Verifies computed argv, environment, and options arrays traverse the runtime +/// marshalling ABI and link into a valid PE image. +#[test] +fn test_windows_proc_open_dynamic_arrays_compile() { + compile_windows_pe( + r#" 'café 日本語', 'COUNT' => 42, 'RATIO' => 1.5, 'YES' => true]; +$options = ['bypass_shell' => true]; +$process = proc_open($command, [['pipe', 'r'], ['pipe', 'w']], $pipes, null, $environment, $options); +proc_close($process); +"#, + ); +} + +/// Verifies dynamic associative command arrays and numeric/raw environment +/// entries compile through the php-src-compatible marshalling paths. +#[test] +fn test_windows_proc_open_dynamic_scalar_array_marshalling_compile() { + compile_windows_pe( + r#" 'cmd.exe', 8 => '/d', 'switch' => '/c', 'body' => 'exit 0']; +$environment = [4 => 'RAW=1', '=C:' => 'C:\\Temp', 'DROP' => false, 'COUNT' => 42]; +$process = proc_open($command, [["pipe", "r"], ["pipe", "w"]], $pipes, null, $environment); +if ($process !== false) { proc_close($process); } +"#, + ); +} + +/// Compiles process status and termination together so the PE linker resolves +/// the retained-command registry, `GetProcessId`, `GetExitCodeProcess`, and +/// `TerminateProcess` without consuming the process before `proc_close`. The +/// numeric string signal also covers weak scalar coercion in x86_64 lowering. +#[test] +fn test_windows_proc_status_and_terminate_compile() { + compile_windows_pe( + r#" NUL'], [1 => ['pipe', 'w']], $pipes); +if ($process === false) { echo 'false'; } +else { + $status = proc_get_status($process); + echo $status['command'] . ':' . $status['pid'] . ':' . ($status['cached'] ? '1' : '0'); + echo proc_terminate($process, "15") ? ':terminated' : ':failed'; + proc_close($process); +} +"#, + ); +} + +/// Runs the Windows process-status path and verifies php-src's Windows-specific +/// contract: all nine status fields exist, `cached` remains false, and +/// `proc_terminate` uses `TerminateProcess(..., 255)` before `proc_close`. +#[test] +fn test_windows_proc_status_and_terminate_run() { + compile_and_run_windows( + r#" NUL'], [1 => ['pipe', 'w']], $pipes); +if ($process === false) { + echo 'false'; +} else { + $status = proc_get_status($process); + $all = isset( + $status['command'], $status['pid'], $status['cached'], + $status['running'], $status['signaled'], $status['stopped'], + $status['exitcode'], $status['termsig'], $status['stopsig'], + ); + echo $all ? 'keys' : 'missing'; + echo $status['cached'] ? ':cached' : ':fresh'; + echo proc_terminate($process) ? ':terminated:' : ':failed:'; + echo proc_close($process); +} +"#, + "keys:fresh:terminated:255", + ); +} + +/// Executes the pre-existing string-command proc_open path to distinguish +/// general CreateProcessW regressions from computed-array marshalling defects. +#[test] +fn test_windows_proc_open_static_command_run() { + compile_and_run_windows( + r#"proc-static.txt', [['pipe', 'w']], $pipes); +if ($process === false) { echo 'false'; } else { + echo proc_close($process) . ':' . trim(file_get_contents('proc-static.txt')); +} +"#, + "0:static-ok", + ); +} + +/// Executes computed proc_open settings under native Windows/Wine and verifies +/// case-insensitive environment de-duplication uses the last PHP entry. A +/// directly executed helper observes the environment without shell expansion. +#[test] +fn test_windows_proc_open_dynamic_arrays_run() { + compile_and_run_windows_expect_code_with_helpers( + r#" 'first', 'ELEPHC_DYNAMIC' => 'second']; +$options = ['bypass_shell' => true]; +$process = proc_open($command, [0 => ['null']], $pipes, null, $environment, $options); +if ($process === false) { echo 'false'; } else { + echo proc_close($process) . ':' . trim(file_get_contents('proc-env.txt')); +} +"#, + "0:second", + 0, + &[( + "proc-env-helper", + " ['null']], $pipes); +if ($process === false) { echo 'false'; } else { + echo proc_close($process) . ':' . trim(file_get_contents('proc-command.txt')); +} +"#, + "0:argv-ok", + 0, + &[( + "proc-command-helper", + " true]; +$process = proc_open($command, [0 => ['null']], $pipes, null, null, $options); +if ($process === false) { echo 'false'; } else { + echo proc_close($process) . ':' . trim(file_get_contents('proc-options.txt')); +} +"#, + "0:options-ok", + 0, + &[( + "proc-options-helper", + " 1, + 'suppress_errors' => true, + 'blocking_pipes' => false, + 'create_process_group' => true, + 'create_new_console' => true, + 'ignored_by_php' => 'value', +]; +$process = proc_open('cmd.exe /d /s /c exit 0', [["pipe", "w"]], $pipes, null, null, $options); +proc_close($process); +"#, + ); +} + +/// Verifies `disk_free_space`/`disk_total_space` compile to a valid Windows +/// PE32+ binary. Compile-only because the returned byte counts depend on the +/// host volume and are exercised by CI under Wine; this catches link failures +/// from the `__rt_sys_statfs` shim (which now calls `GetDiskFreeSpaceExA` +/// instead of the old `xor rax, rax` stub) and resolves the new +/// `GetDiskFreeSpaceExA` import. +#[test] +fn test_windows_disk_free_space_compile() { + compile_windows_pe(r#"modify("+400 years")->format("Y-m-d H:i:s P I"), "|"; +$a = hrtime(true); +$b = hrtime(true); +echo $b >= $a ? "monotonic" : "backwards"; +"#); +} + +/// Verifies the PDO SQLite bridge cross-builds and links into PE/COFF while +/// preserving Unicode SQL/text through the generated extern-call surface. +#[test] +fn test_windows_pdo_sqlite_unicode_compile() { + compile_windows_pe(r#"exec("CREATE TABLE données (valeur TEXT)"); +$pdo->exec("INSERT INTO données VALUES ('été 日本語')"); +$stmt = $pdo->query("SELECT valeur FROM données"); +echo $stmt->fetchColumn(); +"#); +} + +/// Verifies the statically linked image bridge's principal GD, codec, Imagick, +/// built-in-font, Cairo, and Unicode-path call surfaces assemble and link into +/// a Windows PE binary. Codec rendering is exercised on native Windows CI. +#[test] +fn test_windows_image_bridge_primary_operations_compile() { + compile_windows_pe(r#"newImage(8, 8, "blue", "png"); +$wand->resizeImage(4, 4, Imagick::FILTER_LANCZOS, 1.0); +$wand->writeImage("imagick-é東京.png"); + +$surface = new CairoImageSurface(CairoFormat::ARGB32, 8, 8); +$context = new CairoContext($surface); +$context->setSourceRgb(0.0, 1.0, 0.0); +$context->paint(); +$surface->writeToPng("cairo-é東京.png"); +"#); +} + +/// Verifies the pure-Rust crypto and PHAR bridges cross-link into PE/COFF for +/// one-shot HMAC/incremental hashes plus Unicode archive paths and write features. +#[test] +fn test_windows_crypto_and_phar_unicode_compile() { + compile_windows_pe(r#"addFromString("répertoire/東京.txt", "contenu"); +$phar->setMetadata(["langue" => "français"]); +$phar->setSignatureAlgorithm(Phar::SHA256); +$gzip = $phar->compress(Phar::GZ); +echo $gzip["répertoire/東京.txt"]->getContent(); +"#); +} diff --git a/tests/error_tests/io_builtins/streams.rs b/tests/error_tests/io_builtins/streams.rs index f6a765152f..0464be693d 100644 --- a/tests/error_tests/io_builtins/streams.rs +++ b/tests/error_tests/io_builtins/streams.rs @@ -840,6 +840,124 @@ fn test_error_pclose_requires_resource() { ); } +/// Verifies proc_open rejects too few arguments at compile time. +#[test] +fn test_error_proc_open_wrong_args() { + expect_error( + r#" "C"]); +"#, + "proc_open() non-null $env_vars is currently supported only for the Windows target", + ); +} + +/// Verifies non-null Windows/process options receive the precise marshalling +/// gap instead of being accepted and then discarded. +#[test] +fn test_error_proc_open_non_null_options_not_yet_supported() { + expect_error( + r#" true]); +"#, + "proc_open() non-null $options is currently supported only for the Windows target", + ); +} + +/// Verifies proc_close rejects too few arguments at compile time. +#[test] +fn test_error_proc_close_wrong_args() { + expect_error(" 1, 'compound' => []]; escapeshellarg($values['compound']);", + "escapeshellarg() argument #1 ($arg) must be string", + ); +} + +/// Verifies `escapeshellcmd()` rejects a declared union containing an object member. +#[test] +fn test_error_escapeshellcmd_compound_union_type() { + expect_error( + " strlen($before) ? "W" : "!"; echo ":"; -echo sys_get_temp_dir(); +echo strlen(sys_get_temp_dir()) > 0 ? "T" : "!"; "#; + // The temporary directory is checked by marker, like the working directory + // above it: php resolves it from TMPDIR, so it is a per-user path on macOS + // rather than the "/tmp" literal this fixture used to pin. assert_eq!( compile_and_run_ir_backend("working_directory", source), - "CMDW:/tmp" + "CMDW:T" ); } diff --git a/tests/lexer_tests/constants.rs b/tests/lexer_tests/constants.rs index b07c745936..33a71a00bb 100644 --- a/tests/lexer_tests/constants.rs +++ b/tests/lexer_tests/constants.rs @@ -9,13 +9,19 @@ use super::*; -/// Verifies `PHP_EOL`, `PHP_OS`, `DIRECTORY_SEPARATOR` tokenize as runtime constant tokens. +/// Verifies `PHP_EOL`, `PHP_OS`, `DIRECTORY_SEPARATOR`, `PATH_SEPARATOR` tokenize as +/// runtime constant tokens. #[test] fn test_runtime_constant_tokens() { - let t = tokens("