diff --git a/AGENTS.md b/AGENTS.md index 4e9e25f3ec..fcefe67523 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -254,8 +254,14 @@ Key invariants: - **Separate surfaces still need hand-wiring** when relevant: the EIR emitter/runtime routine, optimizer effects (`src/optimize/effects/builtins.rs`), and the runtime-callable wrapper exclusion (`src/codegen/callable_dispatch.rs`). -- `isset`/`unset`/`empty`/`exit`/`die`/`buffer_*` are language constructs that stay +- `isset`/`unset`/`empty`/`exit`/`die` are language constructs that stay checker-resident (`numeric`/`arrays` `check_builtin`), not in the registry. + `buffer_new` is catalog-name-only (its call form is dedicated syntax); `buffer_len` + and `buffer_free` are ordinary registry builtins. +- **elephc-only builtins declare `extension: true`** so `--strict-php` hides them from + user programs (pinned in `src/builtins/parity_tests.rs`). Injected preludes must call + `internal: true` `__elephc_*` aliases instead of PHP-visible extension builtins; a + parity gate scans the prelude sources to enforce this. - Add codegen + error tests (include a case-insensitive or namespaced call for PHP-visible builtins); keep the parity gates in `src/builtins/parity_tests.rs` green. - Before opening a PR that adds, removes, or changes PHP-visible builtins, run the @@ -523,16 +529,23 @@ sidebar: ## Roadmap management -`ROADMAP.md` tracks all planned and completed work, organized by version. +`ROADMAP.md` is the planning document, organized by version. It stays as it is: +- **Do not add entries to record implemented work.** Implementations are documented in `CHANGELOG.md` under `[Unreleased]` (see below). The roadmap only gains new items when work is being *planned*, under the appropriate future version. +- When an implementation completes an item **already present** in the roadmap, mark it `[x]` in place. If no matching item exists, the roadmap is left untouched. - **Never remove completed items** from a version section. Mark them as `[x]` and leave them under the version they belong to. This preserves the history of what was delivered in each release. -- New work items go under the appropriate future version. - When all items in a version are completed, the version is considered done — do not move items elsewhere. ## Changelog management `CHANGELOG.md` records every released version, newest first, in *Keep a Changelog* style. +**Every implementation lands a bullet under the `## [Unreleased]` section in the same PR** — one terse, user-facing entry describing what shipped, not how it was implemented. If the `[Unreleased]` section does not exist, add it at the top of the file (under the header, above the newest version section) together with its compare link at the top of the link list at the end of the file: + +``` +[Unreleased]: https://github.com/illegalstudio/elephc/compare/v...HEAD +``` + When cutting a release: - Add a new section at the top (under the header), above the previous version: diff --git a/CHANGELOG.md b/CHANGELOG.md index 12dd7c822f..ac862e5513 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to elephc, a PHP-to-native compiler written in Rust. Releases are listed newest first. ## [Unreleased] +- Added the `--strict-php` flag: the compiler accepts only PHP-compatible constructs. Extension syntax (`ifdef`, `packed class`, `extern`, `ptr_cast`, `buffer_new`, typed local declarations, `ptr`/`buffer` annotations) is rejected at compile time with per-violation diagnostics across the main file, includes, and autoloaded files, while extension builtins (`ptr_*`, `zval_*`, `buffer_*`, `class_attribute_*`) behave exactly as under the PHP interpreter — `function_exists()` reports `false`, calling one is an undefined function with a hint naming the disabled extension, and user code may declare its own functions with those names. Strict mode also reaches `eval()` with PHP's execute-time semantics: extension builtins do not exist inside eval'd fragments (runtime fatal on call, coherent `function_exists`/`is_callable`), extension syntax in a fragment is a runtime parse error, and user functions shadowing extension names stay callable. Programs using compiler preludes (PDO, timezone, image, web) keep compiling; `--define` cannot be combined with the flag. - Added PHP-compatible sessions to `--web` across PHP 8.2–8.5 and every supported target: `$_SESSION`, the complete `session_*()` API and interfaces, file persistence with `flock`, the `php`/`php_binary`/`php_serialize` wire formats, custom save handlers, strict mode, lazy writes, GC, SID and runtime INI configuration, cookies and cache limiters, auto-start, multipart upload progress, and trans-SID rewriting. Session state, locks, and bridge buffers are reset between requests, while invalid names, IDs, serialized input, and every supported callable-handler form follow PHP-compatible validation and failure paths. The web prelude now retains session helpers by reachability, omits the legacy callable-handler adapter when unused, uses a compact auto-start path, and shares callable descriptors and invokers module-wide, keeping ordinary `--web` compilation and assembly size close to the pre-session baseline. - Fixed checked-arithmetic Mixed boxes leaking when consumed directly by a parent operation (issue #500): the boxed `int|float` result of runtime `+`/`-`/`*` (and unary `-`) is now released after it is coerced for `%`, bitwise/shift operators, `**`, comparisons, and int-coerced or mixed-key array indexes, so shapes like `($i * 7 + 1) & 0xFFFF` or `$SIN[($i * 7 + 5) & 1023]` in tight loops no longer exhaust the heap. Assignment, call-argument, chained mixed-op, and string-coercion releases stay balanced (no double frees), overflow-to-float promotion is unchanged, and heap-debug stays clean with the EIR optimizer enabled or disabled. - Added PHP-compatible `mb_strlen()` to both native compilation and the Magician `eval()` runtime, including the nullable optional `$encoding` argument, UTF-8 malformed-sequence handling, byte-count aliases, iconv-backed multibyte encodings, callable dispatch, and catchable `ValueError` for unknown encodings on every supported target. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d7b099ac5b..677288ee90 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -327,7 +327,7 @@ omitted: `name`, `area`, `params`, `variadic?`, `min_args?`, `max_args?`, `arity_error?`, `returns`, `returns_fresh_storage?`, `by_ref_return?`, `check?`, `lazy_check?`, `lower`, -`summary`, `examples?`, `php_manual?`, `deprecation?`, `internal?`. +`summary`, `examples?`, `php_manual?`, `deprecation?`, `extension?`, `internal?`. - **`params`** — `[name: TypeSpec, name: TypeSpec = DefaultSpec::Variant, ...]`. A parameter with `= DefaultSpec::…` is optional; without it, required. Prefix a @@ -353,6 +353,9 @@ omitted: tighter/looser than its declared parameter list, or needs a verbatim error message. - **`summary` / `examples` / `php_manual` / `deprecation`** — documentation metadata surfaced by the `gen_builtins` exporter. +- **`extension: true`** — an elephc extension with no PHP equivalent (`ptr_*`, + `zval_*`, `buffer_*`, `class_attribute_*`, …). `--strict-php` hides it from user + programs; update `EXPECTED_EXTENSION_BUILTINS` in `src/builtins/parity_tests.rs`. - **`internal: true`** — a compiler-internal builtin that is not PHP-visible and is excluded from catalogs and docs. @@ -447,10 +450,25 @@ when relevant: ### 8. Not every "builtin" is a function -A small set of PHP language constructs — `isset`, `unset`, `empty`, `exit`, `die`, plus -the `buffer_*` intrinsics — are l-value/lazy constructs with dedicated EIR paths and are -intentionally kept in the checker (`numeric`/`arrays` `check_builtin`), not in the -registry. Do not migrate those into `builtin!`. +A small set of PHP language constructs — `isset`, `unset`, `empty`, `exit`, `die` — are +l-value/lazy constructs with dedicated EIR paths and are intentionally kept in the +checker (`numeric`/`arrays` `check_builtin`), not in the registry. Do not migrate those +into `builtin!`. `buffer_new` is similar (its call form is dedicated syntax lowered as +`ExprKind::BufferNew`; only its name lives in the catalog), while `buffer_len` and +`buffer_free` are ordinary registry builtins under `src/builtins/pointers/`. + +Builtins that are elephc extensions with no PHP equivalent must declare +`extension: true` in `builtin!` so `--strict-php` hides them from user programs; the +pinned set lives in `src/builtins/parity_tests.rs` (`EXPECTED_EXTENSION_BUILTINS`). +Injected compiler preludes must never call a PHP-visible extension builtin — use an +`internal: true` `__elephc_*` alias instead (see `src/builtins/pointers/__elephc_ptr_read_string.rs`); +the `preludes_never_call_php_visible_extension_builtins` gate enforces this. + +On the eval side, magician derives its extension set from `EvalArea::RawMemory` +plus the `SYMBOLS_EXTENSION_BUILTINS` list (`crates/elephc-magician/src/interpreter/builtins/spec.rs`) +instead of a per-declaration flag; the `extension_builtin_sets_agree_across_registries` +gate in `tests/builtin_parity_tests.rs` pins that derivation against the compiler +registry, so adding an extension builtin to either registry forces both sides to agree. ## Adding functionality via a Rust crate (bridge crates) diff --git a/README.md b/README.md index 48ed84b9d0..88107a7008 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,9 @@ elephc --gc-stats heavy.php # Enable compile-time feature branches elephc --define DEBUG app.php +# Accept only PHP-compatible constructs (reject every elephc extension) +elephc --strict-php app.php + # Print per-phase compiler timings elephc --timings hello.php diff --git a/crates/elephc-magician/src/ffi/context.rs b/crates/elephc-magician/src/ffi/context.rs index 77ee8ef37d..213542cd5c 100644 --- a/crates/elephc-magician/src/ffi/context.rs +++ b/crates/elephc-magician/src/ffi/context.rs @@ -32,6 +32,19 @@ pub extern "C" fn __elephc_eval_context_new() -> *mut ElephcEvalContext { Box::into_raw(Box::new(ElephcEvalContext::new())) } +/// Marks this program's eval bridge as strict-PHP: extension builtins +/// (`ptr_*`, `buffer_*`, `class_attribute_*`) disappear from eval dispatch and +/// introspection, matching the PHP interpreter where those names do not exist. +/// +/// Generated code emits this call while initializing the eval context, only in +/// binaries compiled with `elephc --strict-php`. The flag is thread-local and +/// elephc programs run every eval on the initializing thread, so one call +/// covers the program lifetime. +#[no_mangle] +pub extern "C" fn __elephc_eval_set_strict_php(enabled: u8) { + crate::strict_php_mode::set_strict_php_mode(enabled != 0); +} + /// Frees a process-level eval context handle allocated by the eval bridge. /// /// # Safety diff --git a/crates/elephc-magician/src/interpreter/builtin_metadata.rs b/crates/elephc-magician/src/interpreter/builtin_metadata.rs index a2733e94ea..13859d3468 100644 --- a/crates/elephc-magician/src/interpreter/builtin_metadata.rs +++ b/crates/elephc-magician/src/interpreter/builtin_metadata.rs @@ -12,8 +12,9 @@ use super::builtins::{ eval_builtin_param_names, eval_builtin_signature_shape, eval_date_procedural_alias_names, - eval_declared_builtin_exists, eval_declared_builtin_spec, eval_php_visible_builtin_exists, - eval_php_visible_builtin_function_names, EvalBuiltinDefaultValue, + eval_declared_builtin_exists, eval_declared_builtin_spec, eval_extension_builtin_names, + eval_php_visible_builtin_exists, eval_php_visible_builtin_function_names, + EvalBuiltinDefaultValue, }; /// A compact, comparison-friendly view of an eval builtin call signature. @@ -48,6 +49,14 @@ pub fn php_visible_builtin_is_registry_declared(name: &str) -> bool { eval_declared_builtin_exists(&canonical) } +/// Returns the eval builtins that are elephc extensions (no PHP equivalent), +/// in stable sorted order. Strict-PHP binaries hide exactly this set from eval +/// dispatch and introspection; parity gates compare it against the compiler +/// registry's `extension`-flagged builtins. +pub fn extension_builtin_names() -> &'static [&'static str] { + eval_extension_builtin_names() +} + /// Returns comparison metadata for one eval builtin signature, when named calls are tracked. pub fn builtin_signature_metadata(name: &str) -> Option { let canonical = php_symbol_key(name); diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index e7979d4444..4f3d1a9321 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -121,8 +121,30 @@ fn declared_builtin_registry() -> &'static DeclaredBuiltinRegistry { } /// Looks up a declaratively migrated eval builtin with PHP case-insensitive matching. +/// +/// This is the single resolution choke point for eval builtin dispatch and +/// introspection (`function_exists`/`is_callable` probes), so the strict-PHP +/// filter lives here: in binaries compiled with `--strict-php`, extension +/// builtins resolve to `None` and eval'd code behaves as if the names did not +/// exist, exactly like the PHP interpreter. pub(in crate::interpreter) fn eval_declared_builtin_spec( name: &str, +) -> Option<&'static EvalBuiltinSpec> { + let key = name.trim_start_matches('\\').to_ascii_lowercase(); + let spec = declared_builtin_registry().by_name.get(&key).copied()?; + if crate::strict_php_mode::strict_php_mode() && spec.is_extension() { + return None; + } + Some(spec) +} + +/// Looks up an eval builtin spec WITHOUT the strict-PHP filter. +/// +/// Metadata derivations (the extension-name list itself, docs exporters) need +/// the raw registry regardless of the thread's strict state; every dispatch or +/// introspection path must use `eval_declared_builtin_spec` instead. +pub(in crate::interpreter) fn eval_raw_declared_builtin_spec( + name: &str, ) -> Option<&'static EvalBuiltinSpec> { let key = name.trim_start_matches('\\').to_ascii_lowercase(); declared_builtin_registry().by_name.get(&key).copied() diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 50bff7f4ac..57edbe08fb 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -8,7 +8,10 @@ //! - Declarative specs are the source of truth for PHP-visible eval builtin names. //! - Lookup callers pass canonical lowercase PHP symbol names. -use super::{eval_declared_builtin_exists, eval_declared_builtin_function_names}; +use super::{ + eval_declared_builtin_exists, eval_declared_builtin_function_names, + eval_raw_declared_builtin_spec, +}; /// Returns the eval interpreter's PHP-visible builtin names. pub(in crate::interpreter) fn eval_php_visible_builtin_function_names() -> &'static [&'static str] { @@ -19,3 +22,23 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_function_names() -> &'sta pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> bool { eval_declared_builtin_exists(name) } + +/// Returns the eval builtins that are elephc extensions (no PHP equivalent), +/// in stable sorted order. Strict-PHP binaries hide exactly this set from eval +/// dispatch and introspection. Derived from the RAW registry so the snapshot +/// is independent of the thread's strict-mode state. +pub(in crate::interpreter) fn eval_extension_builtin_names() -> &'static [&'static str] { + static NAMES: std::sync::OnceLock> = std::sync::OnceLock::new(); + NAMES + .get_or_init(|| { + eval_declared_builtin_function_names() + .iter() + .copied() + .filter(|name| { + eval_raw_declared_builtin_spec(name) + .is_some_and(|spec| spec.is_extension()) + }) + .collect() + }) + .as_slice() +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/tests.rs b/crates/elephc-magician/src/interpreter/builtins/registry/tests.rs index 832db3c675..b3fb9b93d2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/tests.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/tests.rs @@ -16,5 +16,6 @@ mod metadata_misc; mod metadata_regex; mod metadata_streams; mod metadata_time_and_env; +mod strict_mode; use super::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/tests/strict_mode.rs b/crates/elephc-magician/src/interpreter/builtins/registry/tests/strict_mode.rs new file mode 100644 index 0000000000..c91540272a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/tests/strict_mode.rs @@ -0,0 +1,59 @@ +//! Purpose: +//! Registry tests for strict-PHP mode: extension builtins must disappear from +//! declarative lookup (and therefore from dispatch, `function_exists`, and +//! `is_callable`) when the compiled binary was built with `--strict-php`. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - The strict flag is thread-local: strictness is a property of the whole +//! compiled binary, but elephc programs run eval on a single thread, and the +//! thread-local keeps parallel unit tests isolated from each other. + +use super::*; +use crate::strict_php_mode::{scoped_enable, strict_php_mode}; + +/// Verifies strict mode hides extension builtins from declarative lookup while +/// genuine PHP builtins stay resolvable. The RAII guard restores the previous +/// state even if an assertion panics mid-test. +#[test] +fn strict_mode_hides_extension_builtins_from_lookup() { + let _guard = scoped_enable(); + assert!( + eval_declared_builtin_spec("ptr_get").is_none(), + "strict mode must hide ptr_get from eval lookup" + ); + assert!( + eval_declared_builtin_spec("buffer_new").is_none(), + "strict mode must hide buffer_new from eval lookup" + ); + assert!( + eval_declared_builtin_spec("class_attribute_names").is_none(), + "strict mode must hide class_attribute_names from eval lookup" + ); + assert!( + eval_declared_builtin_spec("strlen").is_some(), + "strlen must stay resolvable in strict mode" + ); + assert!( + !eval_php_visible_builtin_exists("ptr_read8"), + "existence probes must honor strict mode" + ); +} + +/// Verifies the flag defaults to off and the guard restores it, so non-strict +/// binaries keep the full extension surface. +#[test] +fn strict_mode_defaults_off_and_guard_restores() { + assert!(!strict_php_mode(), "strict mode must default to off"); + assert!( + eval_declared_builtin_spec("ptr_get").is_some(), + "extension builtins stay visible without strict mode" + ); + { + let _guard = scoped_enable(); + assert!(strict_php_mode(), "scoped_enable must be observable"); + } + assert!(!strict_php_mode(), "dropping the guard must restore the state"); +} diff --git a/crates/elephc-magician/src/interpreter/builtins/spec.rs b/crates/elephc-magician/src/interpreter/builtins/spec.rs index 4ddd8c2a15..601a0d4238 100644 --- a/crates/elephc-magician/src/interpreter/builtins/spec.rs +++ b/crates/elephc-magician/src/interpreter/builtins/spec.rs @@ -103,12 +103,33 @@ pub(in crate::interpreter) struct EvalBuiltinSpec { pub(in crate::interpreter) home_file: &'static str, } +/// Symbols-area builtins that are elephc extensions (attribute introspection +/// without a PHP equivalent). The rest of the extension surface is exactly the +/// `RawMemory` area, so `is_extension` derives from area + this list instead of +/// growing every `eval_builtin!` arm with a new field. The cross-registry gate +/// `extension_builtin_sets_agree_across_registries` in the elephc workspace +/// pins this derivation against the compiler registry's `extension` flags. +const SYMBOLS_EXTENSION_BUILTINS: &[&str] = &[ + "class_attribute_args", + "class_attribute_names", + "class_get_attributes", +]; + impl EvalBuiltinSpec { /// Returns this builtin's file-layout area. pub(in crate::interpreter) fn area(&self) -> EvalArea { self.area } + /// Returns whether this builtin is an elephc extension with no PHP + /// equivalent. Strict-PHP binaries hide extension builtins from eval + /// dispatch and introspection so eval'd code behaves like the PHP + /// interpreter, where these names do not exist. + pub(in crate::interpreter) fn is_extension(&self) -> bool { + matches!(self.area, EvalArea::RawMemory) + || SYMBOLS_EXTENSION_BUILTINS.contains(&self.name) + } + /// Returns the number of required leading parameters. pub(in crate::interpreter) fn required_param_count(&self) -> usize { if let Some(required_param_count) = self.required_param_count { diff --git a/crates/elephc-magician/src/lib.rs b/crates/elephc-magician/src/lib.rs index 0fdac35156..afeca85c74 100644 --- a/crates/elephc-magician/src/lib.rs +++ b/crates/elephc-magician/src/lib.rs @@ -24,6 +24,7 @@ mod parse_cache; pub mod parser; pub mod runtime_hooks; pub mod scope; +mod strict_php_mode; mod stream_resources; mod stream_wrappers; pub mod value; diff --git a/crates/elephc-magician/src/strict_php_mode.rs b/crates/elephc-magician/src/strict_php_mode.rs new file mode 100644 index 0000000000..3523be165c --- /dev/null +++ b/crates/elephc-magician/src/strict_php_mode.rs @@ -0,0 +1,59 @@ +//! Purpose: +//! Owns the strict-PHP mode state for runtime eval: binaries compiled with +//! `elephc --strict-php` hide elephc-extension builtins from eval dispatch and +//! introspection so eval'd code behaves like the PHP interpreter. +//! +//! Called from: +//! - `crate::ffi::context::__elephc_eval_set_strict_php` (generated code sets +//! the flag once while initializing the eval context). +//! - `crate::interpreter::builtins::registry::eval_declared_builtin_spec` +//! (the single lookup choke point consults it on every resolution). +//! +//! Key details: +//! - Thread-local, mirroring the compiler's `strict_php` state: strictness is +//! a property of the whole compiled binary, and elephc programs execute the +//! setter and every eval fragment on one thread (fibers switch stacks, not +//! OS threads), while parallel `cargo test` threads stay isolated. + +use std::cell::Cell; + +thread_local! { + /// Whether the compiled binary embedding this bridge was built with `--strict-php`. + static STRICT_PHP_MODE: Cell = const { Cell::new(false) }; +} + +/// Enables or disables strict-PHP mode for eval on the current thread. +pub(crate) fn set_strict_php_mode(enabled: bool) { + STRICT_PHP_MODE.with(|cell| cell.set(enabled)); +} + +/// Returns whether strict-PHP mode is active for eval on the current thread. +pub(crate) fn strict_php_mode() -> bool { + STRICT_PHP_MODE.with(|cell| cell.get()) +} + +/// RAII guard restoring the previous strict-mode state on drop. +/// +/// Test fixtures hold one of these instead of calling `set_strict_php_mode` in +/// pairs, so a panicking assertion cannot leak strict state into later +/// fixtures on the same thread. +#[cfg(test)] +pub(crate) struct StrictModeGuard { + previous: bool, +} + +#[cfg(test)] +impl Drop for StrictModeGuard { + /// Restores the strict-mode state captured when the guard was created. + fn drop(&mut self) { + set_strict_php_mode(self.previous); + } +} + +/// Enables strict mode and returns a guard that restores the previous state on drop. +#[cfg(test)] +pub(crate) fn scoped_enable() -> StrictModeGuard { + let previous = strict_php_mode(); + set_strict_php_mode(true); + StrictModeGuard { previous } +} diff --git a/docs/beyond-php/buffers.md b/docs/beyond-php/buffers.md index 889a110074..cbca6031f5 100644 --- a/docs/beyond-php/buffers.md +++ b/docs/beyond-php/buffers.md @@ -7,6 +7,8 @@ sidebar: `buffer` is a fixed-size contiguous array of POD values or packed records. Designed for game loops, renderers, and performance-critical code where hash table overhead is unacceptable. +> **Strict mode:** buffers are an elephc extension with no PHP equivalent. Compiling with [`--strict-php`](../compiling/cli-reference.md#strict-php-mode) rejects the `buffer` type, `buffer_new`, `buffer_len()`, and `buffer_free()`. + ## Why not PHP arrays? PHP arrays are hash tables. Every access goes through hashing, linear probing, entry comparison. `buffer` compiles to a single `ldr` instruction: `base + 16 + index * stride`. diff --git a/docs/beyond-php/extern.md b/docs/beyond-php/extern.md index 62efa37944..89a6e0ee6a 100644 --- a/docs/beyond-php/extern.md +++ b/docs/beyond-php/extern.md @@ -7,6 +7,8 @@ sidebar: FFI lets elephc programs call C library functions directly, with automatic type marshalling. +> **Strict mode:** `extern` declarations are an elephc extension with no PHP equivalent. Compiling with [`--strict-php`](../compiling/cli-reference.md#strict-php-mode) rejects them. + ## Declaring extern functions ```php **Strict mode:** `ifdef` is an elephc extension with no PHP equivalent. Compiling with [`--strict-php`](../compiling/cli-reference.md#strict-php-mode) rejects it (and `--define` cannot be combined with the flag). + ## Syntax ```php **Strict mode:** `packed class` is an elephc extension with no PHP equivalent. Compiling with [`--strict-php`](../compiling/cli-reference.md#strict-php-mode) rejects it. + ## Declaration ```php **Strict mode:** pointers are an elephc extension with no PHP equivalent. Compiling with [`--strict-php`](../compiling/cli-reference.md#strict-php-mode) rejects the `ptr` type, `ptr_cast`, and every `ptr_*` builtin. + ## Creating pointers ```php diff --git a/docs/beyond-php/zval-bridge.md b/docs/beyond-php/zval-bridge.md index 9298e0e831..1c24e8fb9a 100644 --- a/docs/beyond-php/zval-bridge.md +++ b/docs/beyond-php/zval-bridge.md @@ -5,6 +5,10 @@ sidebar: order: 8 --- +> **Strict mode:** the `zval_*` builtins are an elephc extension with no PHP +> equivalent. Compiling with +> [`--strict-php`](../compiling/cli-reference.md#strict-php-mode) rejects them. + The zval bridge is a compiler extension that converts native elephc runtime values into PHP-shaped `zval` structures and back. It is the foundation for linking a compiled program against a real PHP extension shared library: the diff --git a/docs/compiling/cli-reference.md b/docs/compiling/cli-reference.md index 7fb1245315..f1dc3d97e3 100644 --- a/docs/compiling/cli-reference.md +++ b/docs/compiling/cli-reference.md @@ -28,6 +28,7 @@ binary is written next to it, named after the source without its extension. | `--emit-asm` | — | off | Write generated assembly instead of a binary. | | `--emit-ir` | — | off | Print the EIR textual form and stop. | | `--check` | — | off | Run front-end checks only; write nothing. | +| `--strict-php` | — | off | Reject every elephc extension; accept only PHP-compatible constructs. See [Strict PHP mode](#strict-php-mode). | | `--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. | @@ -104,6 +105,49 @@ See [Linking, heap, and conditional compilation](linking-and-conditional-compila | `--heap-size=BYTES` | integer ≥ 65536 | `8388608` (8 MB) | Size of the program's runtime heap. | | `--define SYMBOL` / `--define=SYMBOL` | symbol name | — | Define a compile-time symbol for `ifdef` (repeatable). | +## Strict PHP mode + +| Flag | Values | Default | Description | +|---|---|---|---| +| `--strict-php` | — | off | Accept only PHP-compatible constructs: every elephc extension becomes a compile error. | + +Under `--strict-php` the compiler rejects the [beyond-PHP extensions](../beyond-php/pointers.md) +at the source level: + +- extension syntax — `ifdef` blocks, `packed class`, `extern` declarations, + `ptr_cast(...)`, `buffer_new(...)`, typed local variable declarations + (`int $x = 5;`), and `ptr`/`buffer` type annotations — is reported with a + `rejected by --strict-php` diagnostic, one error per violation, wherever the + construct appears (statement bodies, closures, class members, and PHP + attribute arguments alike); +- extension builtins (`ptr_*`, `zval_*`, `buffer_*`, `class_attribute_*`) behave + as if they did not exist, exactly as under the PHP interpreter: + `function_exists()` returns `false` for them, calling one is an undefined + function (the diagnostic names the disabled extension), and user code may + declare its own functions with those names; +- names prefixed with `__elephc_` are reserved for the compiler and rejected in + user code. + +The audit covers the main file plus every `include`/`require`d and autoloaded +user file. Compiler-injected preludes (PDO, timezone, image, web, …) are exempt, +so programs using those PHP-level APIs keep compiling in strict mode. + +Strict mode also reaches `eval()`, matching PHP's runtime semantics for eval'd +code: the compiled binary marks the eval bridge as strict, so extension +builtins do not exist inside eval'd fragments either — calling one is a runtime +fatal (like any unknown function in eval), `function_exists()`/`is_callable()` +report them as missing, and extension syntax in a fragment is a runtime parse +error. Fragments are never rejected at compile time: PHP only fails eval'd code +when it actually executes, and strict mode preserves that. User functions that +shadow extension names remain callable from eval'd code. + +`--strict-php` cannot be combined with `--define`: defines only feed the `ifdef` +extension, which strict mode rejects. + +Strict mode guarantees that the *constructs* used are PHP-compatible; it does +not change elephc's static-subset semantics. A strict-valid program can still be +rejected by the type checker in places where the PHP interpreter would run it. + ## Diagnostics and debugging | Flag | Values | Default | Description | diff --git a/docs/compiling/compilation-pipeline.md b/docs/compiling/compilation-pipeline.md index 600f09f6e6..4b949e0056 100644 --- a/docs/compiling/compilation-pipeline.md +++ b/docs/compiling/compilation-pipeline.md @@ -18,6 +18,7 @@ PHP source -> tokenize Lexer: text -> tokens -> parse Parser: tokens -> AST (Pratt expression parsing) -> magic-constants lower __FILE__, __DIR__, __LINE__, __FUNCTION__, ... + -> (strict-audit) reject elephc-only constructs (with --strict-php) -> (conditional) apply compiler ifdef branches from --define -> autoload-build discover autoload rules -> resolve resolve include/require and declarations @@ -54,6 +55,11 @@ PHP source abstract syntax tree. - **magic-constants** — magic constants such as `__DIR__` and `__LINE__` are substituted before any later pass sees them. +- **strict-PHP audit** — with [`--strict-php`](cli-reference.md#strict-php-mode), + the freshly parsed AST is audited and every elephc-only construct is reported + before any later pass runs. Included and autoloaded user files get the same + audit where they are parsed (inside resolve / autoload-run), while + compiler-injected preludes are exempt. - **conditional compilation** — `ifdef` branches are resolved using the symbols passed with [`--define`](linking-and-conditional-compilation.md#conditional-compilation). - **resolve / prelude injection / name-resolve** — `include`/`require` are diff --git a/docs/compiling/linking-and-conditional-compilation.md b/docs/compiling/linking-and-conditional-compilation.md index 90ba15a0db..98440df651 100644 --- a/docs/compiling/linking-and-conditional-compilation.md +++ b/docs/compiling/linking-and-conditional-compilation.md @@ -128,7 +128,9 @@ generation, so unused branches are never compiled. ### `--define` / `--define=` -Defines a compile-time symbol. Repeatable. +Defines a compile-time symbol. Repeatable. Cannot be combined with +[`--strict-php`](cli-reference.md#strict-php-mode), which rejects the `ifdef` +extension outright. ```bash elephc --define DEBUG app.php diff --git a/docs/internals/builtins/_internal/__elephc_ptr_is_null.md b/docs/internals/builtins/_internal/__elephc_ptr_is_null.md new file mode 100644 index 0000000000..7aabc17342 --- /dev/null +++ b/docs/internals/builtins/_internal/__elephc_ptr_is_null.md @@ -0,0 +1,41 @@ +--- +title: "__elephc_ptr_is_null() — internals" +description: "Compiler internals for __elephc_ptr_is_null(): lowering path, type checks, and runtime helpers." +sidebar: + order: 456 +--- + +## `__elephc_ptr_is_null()` — internals + +## 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/codegen/lower_inst/builtins/pointers.rs`:51](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/pointers.rs#L51) (`lower_ptr_is_null`) +- **Function symbol**: `lower_ptr_is_null()` + + +### Lowering notes + +- Lowers `ptr_is_null(pointer)` by comparing the raw pointer address to zero. + +## Runtime helpers + +_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ + +## Signature summary + +```php +function __elephc_ptr_is_null(mixed $pointer): bool +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + +## Cross-references + +- _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_ptr_read_string.md b/docs/internals/builtins/_internal/__elephc_ptr_read_string.md new file mode 100644 index 0000000000..4b8f220af0 --- /dev/null +++ b/docs/internals/builtins/_internal/__elephc_ptr_read_string.md @@ -0,0 +1,42 @@ +--- +title: "__elephc_ptr_read_string() — internals" +description: "Compiler internals for __elephc_ptr_read_string(): lowering path, type checks, and runtime helpers." +sidebar: + order: 457 +--- + +## `__elephc_ptr_read_string()` — internals + +## 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/codegen/lower_inst/builtins/pointers.rs`:129](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/pointers.rs#L129) (`lower_ptr_read_string`) +- **Function symbol**: `lower_ptr_read_string()` + + +### Lowering notes + +- Lowers `ptr_read_string(pointer, length)` by copying raw bytes into an owned PHP string. + +## Runtime helpers + +The following runtime helpers are referenced: +- `__rt_ptr_read_string` + +## Signature summary + +```php +function __elephc_ptr_read_string(mixed $pointer, mixed $length): string +``` + +## What the type checker enforces + +- **Arity**: takes exactly 2 arguments. + +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + +## Cross-references + +- _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_ptr_write_string.md b/docs/internals/builtins/_internal/__elephc_ptr_write_string.md new file mode 100644 index 0000000000..c58e9c2583 --- /dev/null +++ b/docs/internals/builtins/_internal/__elephc_ptr_write_string.md @@ -0,0 +1,42 @@ +--- +title: "__elephc_ptr_write_string() — internals" +description: "Compiler internals for __elephc_ptr_write_string(): lowering path, type checks, and runtime helpers." +sidebar: + order: 458 +--- + +## `__elephc_ptr_write_string()` — internals + +## 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/codegen/lower_inst/builtins/pointers.rs`:166](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/pointers.rs#L166) (`lower_ptr_write_string`) +- **Function symbol**: `lower_ptr_write_string()` + + +### Lowering notes + +- Lowers `ptr_write_string(pointer, string)` by copying PHP string bytes into raw memory. + +## Runtime helpers + +The following runtime helpers are referenced: +- `__rt_ptr_write_string` + +## Signature summary + +```php +function __elephc_ptr_write_string(mixed $pointer, mixed $string): int +``` + +## What the type checker enforces + +- **Arity**: takes exactly 2 arguments. + +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + +## Cross-references + +- _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_strtotime_raw.md b/docs/internals/builtins/_internal/__elephc_strtotime_raw.md index e5b9600df2..c67fe36780 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: 456 + order: 459 --- ## `__elephc_strtotime_raw()` — internals diff --git a/docs/internals/builtins/array/count.md b/docs/internals/builtins/array/count.md index d37be19c5c..00d8b75c32 100644 --- a/docs/internals/builtins/array/count.md +++ b/docs/internals/builtins/array/count.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/count.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/count.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1025](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1025) (`lower_count`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1023](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1023) (`lower_count`) - **Function symbol**: `lower_count()` diff --git a/docs/internals/builtins/buffer/buffer_free.md b/docs/internals/builtins/buffer/buffer_free.md index c54011722f..ac45658e08 100644 --- a/docs/internals/builtins/buffer/buffer_free.md +++ b/docs/internals/builtins/buffer/buffer_free.md @@ -9,8 +9,8 @@ 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/buffers.rs`:24](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/buffers.rs#L24) (`lower_buffer_free`) +- **Signature**: [`src/builtins/pointers/buffer_free.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/buffer_free.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/buffers.rs`:25](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/buffers.rs#L25) (`lower_buffer_free`) - **Function symbol**: `lower_buffer_free()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function buffer_free(buffer $buffer): mixed +function buffer_free(buffer $buffer): void ``` ## What the type checker enforces diff --git a/docs/internals/builtins/buffer/buffer_len.md b/docs/internals/builtins/buffer/buffer_len.md index a61052032f..71cef7a2d6 100644 --- a/docs/internals/builtins/buffer/buffer_len.md +++ b/docs/internals/builtins/buffer/buffer_len.md @@ -9,8 +9,8 @@ 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/buffers.rs`:19](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/buffers.rs#L19) (`lower_buffer_len`) +- **Signature**: [`src/builtins/pointers/buffer_len.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/buffer_len.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/buffers.rs`:20](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/buffers.rs#L20) (`lower_buffer_len`) - **Function symbol**: `lower_buffer_len()` diff --git a/docs/internals/builtins/class/class_exists.md b/docs/internals/builtins/class/class_exists.md index 833594322e..a2c4094b05 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/codegen/lower_inst/builtins.rs`:583](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L583) (`lower_class_like_exists`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:581](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L581) (`lower_class_like_exists`) - **Function symbol**: `lower_class_like_exists()` diff --git a/docs/internals/builtins/class/enum_exists.md b/docs/internals/builtins/class/enum_exists.md index 4b0d6369fb..8e7bb5d9ef 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/codegen/lower_inst/builtins.rs`:583](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L583) (`lower_class_like_exists`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:581](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L581) (`lower_class_like_exists`) - **Function symbol**: `lower_class_like_exists()` diff --git a/docs/internals/builtins/class/function_exists.md b/docs/internals/builtins/class/function_exists.md index 448b43c900..f6878aabad 100644 --- a/docs/internals/builtins/class/function_exists.md +++ b/docs/internals/builtins/class/function_exists.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/function_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/function_exists.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:566](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L566) (`lower_function_exists`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:564](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L564) (`lower_function_exists`) - **Function symbol**: `lower_function_exists()` diff --git a/docs/internals/builtins/class/interface_exists.md b/docs/internals/builtins/class/interface_exists.md index 848f61c5fa..b1760484c2 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/codegen/lower_inst/builtins.rs`:583](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L583) (`lower_class_like_exists`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:581](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L581) (`lower_class_like_exists`) - **Function symbol**: `lower_class_like_exists()` diff --git a/docs/internals/builtins/class/trait_exists.md b/docs/internals/builtins/class/trait_exists.md index 4632d523a5..f8ad9df55d 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/codegen/lower_inst/builtins.rs`:583](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L583) (`lower_class_like_exists`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:581](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L581) (`lower_class_like_exists`) - **Function symbol**: `lower_class_like_exists()` diff --git a/docs/internals/builtins/misc/define.md b/docs/internals/builtins/misc/define.md index 37079664aa..7dc7114bff 100644 --- a/docs/internals/builtins/misc/define.md +++ b/docs/internals/builtins/misc/define.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/define.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/define.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:372](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L372) (`lower_define`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:370](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L370) (`lower_define`) - **Function symbol**: `lower_define()` diff --git a/docs/internals/builtins/misc/defined.md b/docs/internals/builtins/misc/defined.md index 83900ba1b5..337af5d778 100644 --- a/docs/internals/builtins/misc/defined.md +++ b/docs/internals/builtins/misc/defined.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/defined.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/defined.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:551](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L551) (`lower_defined`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:549](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L549) (`lower_defined`) - **Function symbol**: `lower_defined()` diff --git a/docs/internals/builtins/misc/empty.md b/docs/internals/builtins/misc/empty.md index 4830ba86d9..fe367e862f 100644 --- a/docs/internals/builtins/misc/empty.md +++ b/docs/internals/builtins/misc/empty.md @@ -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`:1218](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1218) (`lower_empty`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1216](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1216) (`lower_empty`) - **Function symbol**: `lower_empty()` diff --git a/docs/internals/builtins/misc/phpversion.md b/docs/internals/builtins/misc/phpversion.md index 278abe7712..61ca0ecd69 100644 --- a/docs/internals/builtins/misc/phpversion.md +++ b/docs/internals/builtins/misc/phpversion.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/phpversion.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/phpversion.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:541](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L541) (`lower_phpversion`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:539](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L539) (`lower_phpversion`) - **Function symbol**: `lower_phpversion()` diff --git a/docs/internals/builtins/string/strlen.md b/docs/internals/builtins/string/strlen.md index 34c628ad41..872fc4ec8d 100644 --- a/docs/internals/builtins/string/strlen.md +++ b/docs/internals/builtins/string/strlen.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/strlen.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strlen.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1079](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1079) (`lower_strlen`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1077](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1077) (`lower_strlen`) - **Function symbol**: `lower_strlen()` diff --git a/docs/internals/builtins/type/boolval.md b/docs/internals/builtins/type/boolval.md index 549790d675..1230c0e181 100644 --- a/docs/internals/builtins/type/boolval.md +++ b/docs/internals/builtins/type/boolval.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/boolval.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/boolval.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1176](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1176) (`lower_boolval`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1174](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1174) (`lower_boolval`) - **Function symbol**: `lower_boolval()` diff --git a/docs/internals/builtins/type/floatval.md b/docs/internals/builtins/type/floatval.md index 072c15257d..813d03f851 100644 --- a/docs/internals/builtins/type/floatval.md +++ b/docs/internals/builtins/type/floatval.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/floatval.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/floatval.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1142](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1142) (`lower_floatval`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1140](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1140) (`lower_floatval`) - **Function symbol**: `lower_floatval()` diff --git a/docs/internals/builtins/type/gettype.md b/docs/internals/builtins/type/gettype.md index 7272647b30..0baf669ea1 100644 --- a/docs/internals/builtins/type/gettype.md +++ b/docs/internals/builtins/type/gettype.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/gettype.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/gettype.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:419](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L419) (`lower_gettype`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:417](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L417) (`lower_gettype`) - **Function symbol**: `lower_gettype()` diff --git a/docs/internals/builtins/type/intval.md b/docs/internals/builtins/type/intval.md index c77300b93c..8cde0f5d6b 100644 --- a/docs/internals/builtins/type/intval.md +++ b/docs/internals/builtins/type/intval.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/intval.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/intval.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1109](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1109) (`lower_intval`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1107](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1107) (`lower_intval`) - **Function symbol**: `lower_intval()` diff --git a/docs/internals/builtins/type/is_array.md b/docs/internals/builtins/type/is_array.md index 68eca23430..266b6a2189 100644 --- a/docs/internals/builtins/type/is_array.md +++ b/docs/internals/builtins/type/is_array.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_array.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_array.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1603](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1603) (`lower_is_array`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1601](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1601) (`lower_is_array`) - **Function symbol**: `lower_is_array()` diff --git a/docs/internals/builtins/type/is_bool.md b/docs/internals/builtins/type/is_bool.md index 160f7d3d27..c409247bc4 100644 --- a/docs/internals/builtins/type/is_bool.md +++ b/docs/internals/builtins/type/is_bool.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_bool.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_bool.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1336](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1336) (`lower_static_type_predicate`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1334](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1334) (`lower_static_type_predicate`) - **Function symbol**: `lower_static_type_predicate()` diff --git a/docs/internals/builtins/type/is_callable.md b/docs/internals/builtins/type/is_callable.md index 6deaa7685d..664ebca3ae 100644 --- a/docs/internals/builtins/type/is_callable.md +++ b/docs/internals/builtins/type/is_callable.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_callable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_callable.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:712](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L712) (`lower_is_callable`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:710](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L710) (`lower_is_callable`) - **Function symbol**: `lower_is_callable()` diff --git a/docs/internals/builtins/type/is_float.md b/docs/internals/builtins/type/is_float.md index 8097123eac..2a70b99832 100644 --- a/docs/internals/builtins/type/is_float.md +++ b/docs/internals/builtins/type/is_float.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_float.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_float.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1336](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1336) (`lower_static_type_predicate`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1334](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1334) (`lower_static_type_predicate`) - **Function symbol**: `lower_static_type_predicate()` diff --git a/docs/internals/builtins/type/is_int.md b/docs/internals/builtins/type/is_int.md index 26cb7481ef..60d6ab7819 100644 --- a/docs/internals/builtins/type/is_int.md +++ b/docs/internals/builtins/type/is_int.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_int.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_int.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1336](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1336) (`lower_static_type_predicate`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1334](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1334) (`lower_static_type_predicate`) - **Function symbol**: `lower_static_type_predicate()` diff --git a/docs/internals/builtins/type/is_iterable.md b/docs/internals/builtins/type/is_iterable.md index b28fc91b4d..0ceb5c15e8 100644 --- a/docs/internals/builtins/type/is_iterable.md +++ b/docs/internals/builtins/type/is_iterable.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_iterable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_iterable.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1394](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1394) (`lower_is_iterable`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1392](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1392) (`lower_is_iterable`) - **Function symbol**: `lower_is_iterable()` diff --git a/docs/internals/builtins/type/is_null.md b/docs/internals/builtins/type/is_null.md index 038f6c1fb9..43d925866b 100644 --- a/docs/internals/builtins/type/is_null.md +++ b/docs/internals/builtins/type/is_null.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_null.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_null.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1593](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1593) (`lower_is_null_builtin`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1591](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1591) (`lower_is_null_builtin`) - **Function symbol**: `lower_is_null_builtin()` diff --git a/docs/internals/builtins/type/is_object.md b/docs/internals/builtins/type/is_object.md index e5151f84b8..ac062b6fc5 100644 --- a/docs/internals/builtins/type/is_object.md +++ b/docs/internals/builtins/type/is_object.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_object.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_object.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1618](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1618) (`lower_is_object`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1616](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1616) (`lower_is_object`) - **Function symbol**: `lower_is_object()` diff --git a/docs/internals/builtins/type/is_scalar.md b/docs/internals/builtins/type/is_scalar.md index 9b93411339..c2f88a9dea 100644 --- a/docs/internals/builtins/type/is_scalar.md +++ b/docs/internals/builtins/type/is_scalar.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_scalar.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_scalar.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1634](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1634) (`lower_is_scalar`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1632](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1632) (`lower_is_scalar`) - **Function symbol**: `lower_is_scalar()` diff --git a/docs/internals/builtins/type/is_string.md b/docs/internals/builtins/type/is_string.md index 909aea8ce6..1954c30c4a 100644 --- a/docs/internals/builtins/type/is_string.md +++ b/docs/internals/builtins/type/is_string.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_string.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_string.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1336](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1336) (`lower_static_type_predicate`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1334](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1334) (`lower_static_type_predicate`) - **Function symbol**: `lower_static_type_predicate()` diff --git a/docs/php/builtins.md b/docs/php/builtins.md index 3fe1859341..27292c17ac 100644 --- a/docs/php/builtins.md +++ b/docs/php/builtins.md @@ -72,7 +72,7 @@ sidebar: | [`uasort()`](./builtins/array/uasort.md) | `(array $array, callable $callback): bool` | `bool` | ✓ | ✓ | | [`uksort()`](./builtins/array/uksort.md) | `(array $array, callable $callback): bool` | `bool` | ✓ | ✓ | | [`usort()`](./builtins/array/usort.md) | `(array $array, callable $callback): bool` | `bool` | ✓ | ✓ | -| [`buffer_free()`](./builtins/buffer/buffer_free.md) | `(buffer $buffer): mixed` | `mixed` | ✓ | ✓ | +| [`buffer_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` | ✓ | ✓ | | [`class_attribute_args()`](./builtins/class/class_attribute_args.md) | `(string $class_name, string $attribute_name): array` | `array` | ✓ | ✓ | diff --git a/docs/php/builtins/buffer.md b/docs/php/builtins/buffer.md index 7e4d057561..f70c96ea8a 100644 --- a/docs/php/builtins/buffer.md +++ b/docs/php/builtins/buffer.md @@ -9,5 +9,5 @@ sidebar: | Function | Signature | Returns | AOT | eval() | |---|---|---|:-:|:-:| -| [`buffer_free()`](./buffer/buffer_free.md) | `(buffer $buffer): mixed` | `mixed` | ✓ | ✓ | +| [`buffer_free()`](./buffer/buffer_free.md) | `(buffer $buffer): void` | `void` | ✓ | ✓ | | [`buffer_len()`](./buffer/buffer_len.md) | `(buffer $buffer): int` | `int` | ✓ | ✓ | diff --git a/docs/php/builtins/buffer/buffer_free.md b/docs/php/builtins/buffer/buffer_free.md index 88169f317c..4626f3e6e9 100644 --- a/docs/php/builtins/buffer/buffer_free.md +++ b/docs/php/builtins/buffer/buffer_free.md @@ -1,6 +1,6 @@ --- title: "buffer_free()" -description: "Lowers `buffer_free()` through the direct buffer opcode helper." +description: "Frees a buffer and nulls the local variable that held it." sidebar: order: 64 --- @@ -8,20 +8,21 @@ sidebar: ## buffer_free() ```php -function buffer_free(buffer $buffer): mixed +function buffer_free(buffer $buffer): void ``` -Lowers `buffer_free()` through the direct buffer opcode helper. +Frees a buffer and nulls the local variable that held it. **Parameters**: - `$buffer` (`buffer`) -**Returns**: `mixed` +**Returns**: `void` ## Availability - **Compiled (AOT)**: supported by the Elephc code generator. - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_free.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_free.rs)). +- **Strict PHP mode**: hidden — this builtin is an elephc extension with no PHP equivalent, so programs compiled with [`--strict-php`](../../../compiling/cli-reference.md#strict-php-mode) treat the name as nonexistent, in compiled code and inside eval'd code. _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/buffer/buffer_len.md b/docs/php/builtins/buffer/buffer_len.md index 0b7d6d7d3e..16e8ea5b20 100644 --- a/docs/php/builtins/buffer/buffer_len.md +++ b/docs/php/builtins/buffer/buffer_len.md @@ -1,6 +1,6 @@ --- title: "buffer_len()" -description: "Lowers `buffer_len()` through the direct buffer opcode helper." +description: "Returns the logical element count of a buffer." sidebar: order: 65 --- @@ -11,7 +11,7 @@ sidebar: function buffer_len(buffer $buffer): int ``` -Lowers `buffer_len()` through the direct buffer opcode helper. +Returns the logical element count of a buffer. **Parameters**: - `$buffer` (`buffer`) @@ -22,6 +22,7 @@ Lowers `buffer_len()` through the direct buffer opcode helper. - **Compiled (AOT)**: supported by the Elephc code generator. - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_len.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_len.rs)). +- **Strict PHP mode**: hidden — this builtin is an elephc extension with no PHP equivalent, so programs compiled with [`--strict-php`](../../../compiling/cli-reference.md#strict-php-mode) treat the name as nonexistent, in compiled code and inside eval'd code. _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/class_attribute_args.md b/docs/php/builtins/class/class_attribute_args.md index 6302ef4669..d2bff41fa0 100644 --- a/docs/php/builtins/class/class_attribute_args.md +++ b/docs/php/builtins/class/class_attribute_args.md @@ -23,6 +23,7 @@ Returns the constructor arguments of a named attribute applied to a class. - **Compiled (AOT)**: supported by the Elephc code generator. - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs)). +- **Strict PHP mode**: hidden — this builtin is an elephc extension with no PHP equivalent, so programs compiled with [`--strict-php`](../../../compiling/cli-reference.md#strict-php-mode) treat the name as nonexistent, in compiled code and inside eval'd code. _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/class_attribute_names.md b/docs/php/builtins/class/class_attribute_names.md index 4d5120b20b..ff5f799898 100644 --- a/docs/php/builtins/class/class_attribute_names.md +++ b/docs/php/builtins/class/class_attribute_names.md @@ -22,6 +22,7 @@ Returns the list of attribute names applied to a class. - **Compiled (AOT)**: supported by the Elephc code generator. - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs)). +- **Strict PHP mode**: hidden — this builtin is an elephc extension with no PHP equivalent, so programs compiled with [`--strict-php`](../../../compiling/cli-reference.md#strict-php-mode) treat the name as nonexistent, in compiled code and inside eval'd code. _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/class_get_attributes.md b/docs/php/builtins/class/class_get_attributes.md index e02fab8ba2..2aad3e8494 100644 --- a/docs/php/builtins/class/class_get_attributes.md +++ b/docs/php/builtins/class/class_get_attributes.md @@ -22,6 +22,7 @@ Returns an array of ReflectionAttribute objects for all attributes of a class. - **Compiled (AOT)**: supported by the Elephc code generator. - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs)). +- **Strict PHP mode**: hidden — this builtin is an elephc extension with no PHP equivalent, so programs compiled with [`--strict-php`](../../../compiling/cli-reference.md#strict-php-mode) treat the name as nonexistent, in compiled code and inside eval'd code. _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc/buffer_new.md b/docs/php/builtins/misc/buffer_new.md index ca48729c20..e9e66cb9f0 100644 --- a/docs/php/builtins/misc/buffer_new.md +++ b/docs/php/builtins/misc/buffer_new.md @@ -22,5 +22,6 @@ function buffer_new(int $length): mixed - **Compiled (AOT)**: supported by the Elephc code generator. - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_new.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_new.rs)). +- **Strict PHP mode**: hidden — this builtin is an elephc extension with no PHP equivalent, so programs compiled with [`--strict-php`](../../../compiling/cli-reference.md#strict-php-mode) treat the name as nonexistent, in compiled code and inside eval'd code. _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr.md b/docs/php/builtins/pointer/ptr.md index 9508c7a85c..d6668e11d1 100644 --- a/docs/php/builtins/pointer/ptr.md +++ b/docs/php/builtins/pointer/ptr.md @@ -22,6 +22,7 @@ Returns a raw pointer to the given variable. - **Compiled (AOT)**: supported by the Elephc code generator. - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr.rs)). +- **Strict PHP mode**: hidden — this builtin is an elephc extension with no PHP equivalent, so programs compiled with [`--strict-php`](../../../compiling/cli-reference.md#strict-php-mode) treat the name as nonexistent, in compiled code and inside eval'd code. _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_get.md b/docs/php/builtins/pointer/ptr_get.md index bf295807b1..41996f357f 100644 --- a/docs/php/builtins/pointer/ptr_get.md +++ b/docs/php/builtins/pointer/ptr_get.md @@ -22,6 +22,7 @@ Reads one machine word through a raw pointer and returns it as an integer. - **Compiled (AOT)**: supported by the Elephc code generator. - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_get.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_get.rs)). +- **Strict PHP mode**: hidden — this builtin is an elephc extension with no PHP equivalent, so programs compiled with [`--strict-php`](../../../compiling/cli-reference.md#strict-php-mode) treat the name as nonexistent, in compiled code and inside eval'd code. _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_is_null.md b/docs/php/builtins/pointer/ptr_is_null.md index 7c1484e556..0d41cbfee8 100644 --- a/docs/php/builtins/pointer/ptr_is_null.md +++ b/docs/php/builtins/pointer/ptr_is_null.md @@ -22,6 +22,7 @@ Returns true if the pointer is null. - **Compiled (AOT)**: supported by the Elephc code generator. - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_is_null.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_is_null.rs)). +- **Strict PHP mode**: hidden — this builtin is an elephc extension with no PHP equivalent, so programs compiled with [`--strict-php`](../../../compiling/cli-reference.md#strict-php-mode) treat the name as nonexistent, in compiled code and inside eval'd code. _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_null.md b/docs/php/builtins/pointer/ptr_null.md index ed2b38d16f..f532533c71 100644 --- a/docs/php/builtins/pointer/ptr_null.md +++ b/docs/php/builtins/pointer/ptr_null.md @@ -21,6 +21,7 @@ Returns a null raw pointer. - **Compiled (AOT)**: supported by the Elephc code generator. - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_null.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_null.rs)). +- **Strict PHP mode**: hidden — this builtin is an elephc extension with no PHP equivalent, so programs compiled with [`--strict-php`](../../../compiling/cli-reference.md#strict-php-mode) treat the name as nonexistent, in compiled code and inside eval'd code. _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_offset.md b/docs/php/builtins/pointer/ptr_offset.md index d1fe66c738..ebf7a0134a 100644 --- a/docs/php/builtins/pointer/ptr_offset.md +++ b/docs/php/builtins/pointer/ptr_offset.md @@ -23,6 +23,7 @@ Returns a new pointer offset from the given pointer by the given byte count. - **Compiled (AOT)**: supported by the Elephc code generator. - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_offset.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_offset.rs)). +- **Strict PHP mode**: hidden — this builtin is an elephc extension with no PHP equivalent, so programs compiled with [`--strict-php`](../../../compiling/cli-reference.md#strict-php-mode) treat the name as nonexistent, in compiled code and inside eval'd code. _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_read16.md b/docs/php/builtins/pointer/ptr_read16.md index 01fb96f0de..dedd9360c9 100644 --- a/docs/php/builtins/pointer/ptr_read16.md +++ b/docs/php/builtins/pointer/ptr_read16.md @@ -22,6 +22,7 @@ Reads one unsigned 16-bit word through a raw pointer and returns it as an intege - **Compiled (AOT)**: supported by the Elephc code generator. - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read16.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read16.rs)). +- **Strict PHP mode**: hidden — this builtin is an elephc extension with no PHP equivalent, so programs compiled with [`--strict-php`](../../../compiling/cli-reference.md#strict-php-mode) treat the name as nonexistent, in compiled code and inside eval'd code. _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_read32.md b/docs/php/builtins/pointer/ptr_read32.md index 4bb3023d17..2c93a21179 100644 --- a/docs/php/builtins/pointer/ptr_read32.md +++ b/docs/php/builtins/pointer/ptr_read32.md @@ -22,6 +22,7 @@ Reads one unsigned 32-bit word through a raw pointer and returns it as an intege - **Compiled (AOT)**: supported by the Elephc code generator. - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read32.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read32.rs)). +- **Strict PHP mode**: hidden — this builtin is an elephc extension with no PHP equivalent, so programs compiled with [`--strict-php`](../../../compiling/cli-reference.md#strict-php-mode) treat the name as nonexistent, in compiled code and inside eval'd code. _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_read8.md b/docs/php/builtins/pointer/ptr_read8.md index 6f0906e47f..8f076eeeec 100644 --- a/docs/php/builtins/pointer/ptr_read8.md +++ b/docs/php/builtins/pointer/ptr_read8.md @@ -22,6 +22,7 @@ Reads one unsigned byte through a raw pointer and returns it as an integer. - **Compiled (AOT)**: supported by the Elephc code generator. - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read8.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read8.rs)). +- **Strict PHP mode**: hidden — this builtin is an elephc extension with no PHP equivalent, so programs compiled with [`--strict-php`](../../../compiling/cli-reference.md#strict-php-mode) treat the name as nonexistent, in compiled code and inside eval'd code. _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_read_string.md b/docs/php/builtins/pointer/ptr_read_string.md index 52f27f4d03..55b8762ae2 100644 --- a/docs/php/builtins/pointer/ptr_read_string.md +++ b/docs/php/builtins/pointer/ptr_read_string.md @@ -23,6 +23,7 @@ Copies raw bytes from a pointer into a PHP string of the given length. - **Compiled (AOT)**: supported by the Elephc code generator. - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read_string.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read_string.rs)). +- **Strict PHP mode**: hidden — this builtin is an elephc extension with no PHP equivalent, so programs compiled with [`--strict-php`](../../../compiling/cli-reference.md#strict-php-mode) treat the name as nonexistent, in compiled code and inside eval'd code. _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_set.md b/docs/php/builtins/pointer/ptr_set.md index 155d0c8060..f806b9572f 100644 --- a/docs/php/builtins/pointer/ptr_set.md +++ b/docs/php/builtins/pointer/ptr_set.md @@ -23,6 +23,7 @@ Writes one machine word through a raw pointer. - **Compiled (AOT)**: supported by the Elephc code generator. - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_set.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_set.rs)). +- **Strict PHP mode**: hidden — this builtin is an elephc extension with no PHP equivalent, so programs compiled with [`--strict-php`](../../../compiling/cli-reference.md#strict-php-mode) treat the name as nonexistent, in compiled code and inside eval'd code. _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_sizeof.md b/docs/php/builtins/pointer/ptr_sizeof.md index f6a53fa4a2..0ea02d1427 100644 --- a/docs/php/builtins/pointer/ptr_sizeof.md +++ b/docs/php/builtins/pointer/ptr_sizeof.md @@ -22,6 +22,7 @@ Returns the byte size of the named pointer target type. - **Compiled (AOT)**: supported by the Elephc code generator. - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_sizeof.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_sizeof.rs)). +- **Strict PHP mode**: hidden — this builtin is an elephc extension with no PHP equivalent, so programs compiled with [`--strict-php`](../../../compiling/cli-reference.md#strict-php-mode) treat the name as nonexistent, in compiled code and inside eval'd code. _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_write16.md b/docs/php/builtins/pointer/ptr_write16.md index f4f7c45bdd..e26e1b4fa0 100644 --- a/docs/php/builtins/pointer/ptr_write16.md +++ b/docs/php/builtins/pointer/ptr_write16.md @@ -23,6 +23,7 @@ Writes one 16-bit word through a raw pointer. - **Compiled (AOT)**: supported by the Elephc code generator. - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write16.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write16.rs)). +- **Strict PHP mode**: hidden — this builtin is an elephc extension with no PHP equivalent, so programs compiled with [`--strict-php`](../../../compiling/cli-reference.md#strict-php-mode) treat the name as nonexistent, in compiled code and inside eval'd code. _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_write32.md b/docs/php/builtins/pointer/ptr_write32.md index bd624bb964..5f52b55086 100644 --- a/docs/php/builtins/pointer/ptr_write32.md +++ b/docs/php/builtins/pointer/ptr_write32.md @@ -23,6 +23,7 @@ Writes one 32-bit word through a raw pointer. - **Compiled (AOT)**: supported by the Elephc code generator. - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write32.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write32.rs)). +- **Strict PHP mode**: hidden — this builtin is an elephc extension with no PHP equivalent, so programs compiled with [`--strict-php`](../../../compiling/cli-reference.md#strict-php-mode) treat the name as nonexistent, in compiled code and inside eval'd code. _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_write8.md b/docs/php/builtins/pointer/ptr_write8.md index 46d8170bf7..70fd26f287 100644 --- a/docs/php/builtins/pointer/ptr_write8.md +++ b/docs/php/builtins/pointer/ptr_write8.md @@ -23,6 +23,7 @@ Writes one byte through a raw pointer. - **Compiled (AOT)**: supported by the Elephc code generator. - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write8.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write8.rs)). +- **Strict PHP mode**: hidden — this builtin is an elephc extension with no PHP equivalent, so programs compiled with [`--strict-php`](../../../compiling/cli-reference.md#strict-php-mode) treat the name as nonexistent, in compiled code and inside eval'd code. _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_write_string.md b/docs/php/builtins/pointer/ptr_write_string.md index 970f2dd3d4..f5b8cd8374 100644 --- a/docs/php/builtins/pointer/ptr_write_string.md +++ b/docs/php/builtins/pointer/ptr_write_string.md @@ -23,6 +23,7 @@ Copies PHP string bytes into raw memory at the given pointer. - **Compiled (AOT)**: supported by the Elephc code generator. - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write_string.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write_string.rs)). +- **Strict PHP mode**: hidden — this builtin is an elephc extension with no PHP equivalent, so programs compiled with [`--strict-php`](../../../compiling/cli-reference.md#strict-php-mode) treat the name as nonexistent, in compiled code and inside eval'd code. _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/zval_free.md b/docs/php/builtins/pointer/zval_free.md index cd046be29d..304e577256 100644 --- a/docs/php/builtins/pointer/zval_free.md +++ b/docs/php/builtins/pointer/zval_free.md @@ -22,6 +22,7 @@ Frees a PHP zval pointer allocated by `zval_pack`. - **Compiled (AOT)**: supported by the Elephc code generator. - **`eval()` (magician interpreter)**: not available inside eval'd code. +- **Strict PHP mode**: hidden — this builtin is an elephc extension with no PHP equivalent, so programs compiled with [`--strict-php`](../../../compiling/cli-reference.md#strict-php-mode) treat the name as nonexistent, in compiled code and inside eval'd code. _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/zval_pack.md b/docs/php/builtins/pointer/zval_pack.md index ae3b82705f..6fffe9e2fd 100644 --- a/docs/php/builtins/pointer/zval_pack.md +++ b/docs/php/builtins/pointer/zval_pack.md @@ -22,6 +22,7 @@ Packs an elephc runtime value into a heap-allocated PHP zval pointer. - **Compiled (AOT)**: supported by the Elephc code generator. - **`eval()` (magician interpreter)**: not available inside eval'd code. +- **Strict PHP mode**: hidden — this builtin is an elephc extension with no PHP equivalent, so programs compiled with [`--strict-php`](../../../compiling/cli-reference.md#strict-php-mode) treat the name as nonexistent, in compiled code and inside eval'd code. _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/zval_type.md b/docs/php/builtins/pointer/zval_type.md index 6f8351dfb0..fa070dfb47 100644 --- a/docs/php/builtins/pointer/zval_type.md +++ b/docs/php/builtins/pointer/zval_type.md @@ -22,6 +22,7 @@ Returns the PHP zval type byte for a zval pointer. - **Compiled (AOT)**: supported by the Elephc code generator. - **`eval()` (magician interpreter)**: not available inside eval'd code. +- **Strict PHP mode**: hidden — this builtin is an elephc extension with no PHP equivalent, so programs compiled with [`--strict-php`](../../../compiling/cli-reference.md#strict-php-mode) treat the name as nonexistent, in compiled code and inside eval'd code. _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/zval_unpack.md b/docs/php/builtins/pointer/zval_unpack.md index 3d0d4c6aab..35ad4d8b6a 100644 --- a/docs/php/builtins/pointer/zval_unpack.md +++ b/docs/php/builtins/pointer/zval_unpack.md @@ -22,6 +22,7 @@ Unpacks a PHP zval pointer into an owned elephc Mixed value. - **Compiled (AOT)**: supported by the Elephc code generator. - **`eval()` (magician interpreter)**: not available inside eval'd code. +- **Strict PHP mode**: hidden — this builtin is an elephc extension with no PHP equivalent, so programs compiled with [`--strict-php`](../../../compiling/cli-reference.md#strict-php-mode) treat the name as nonexistent, in compiled code and inside eval'd code. _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/eval.md b/docs/php/eval.md index 9df120a23f..651c0bba6d 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -877,6 +877,15 @@ Eval builtin dispatch supports direct calls, named arguments, callable dispatch, `call_user_func()`, `call_user_func_array()`, and `function_exists()` where listed below unless a note says otherwise. +> **Strict mode:** in binaries compiled with +> [`--strict-php`](../compiling/cli-reference.md#strict-php-mode), the +> elephc-extension builtins below — the whole "Raw memory and buffers" row plus +> `class_attribute_names()`, `class_attribute_args()`, and +> `class_get_attributes()` — do not exist inside eval'd code either: calling one +> is a runtime fatal like any unknown function, and +> `function_exists()`/`is_callable()` report them as missing, matching the PHP +> interpreter. + | Area | Builtins | |---|---| | System, time, and environment | `time()`, `microtime()`, `hrtime()`, `date()`, `gmdate()`, `mktime()`, `gmmktime()`, `checkdate()`, `getdate()`, `localtime()`, `strtotime()`, `date_default_timezone_get()`, `date_default_timezone_set()`, `http_response_code()`, `header()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()` | diff --git a/examples/strict-php/.gitignore b/examples/strict-php/.gitignore new file mode 100644 index 0000000000..fc9576aeb9 --- /dev/null +++ b/examples/strict-php/.gitignore @@ -0,0 +1,3 @@ +*.s +*.o +main diff --git a/examples/strict-php/main.php b/examples/strict-php/main.php new file mode 100644 index 0000000000..dd694fd8aa --- /dev/null +++ b/examples/strict-php/main.php @@ -0,0 +1,28 @@ +'; +} + +$slots = ['alpha', 'beta', 'gamma']; + +echo "ptr_get is ours: ", ptr_get($slots, 1), "\n"; +echo "function_exists('ptr_get'): ", var_export(function_exists('ptr_get'), true), "\n"; +echo "function_exists('zval_pack'): ", var_export(function_exists('zval_pack'), true), "\n"; + +// Plain PHP keeps working as usual. +$total = 0; +foreach ($slots as $name) { + $total += strlen($name); +} +echo "total name length: {$total}\n"; diff --git a/scripts/docs/audit_builtins.py b/scripts/docs/audit_builtins.py index 4c738225c8..e57c5714d2 100644 --- a/scripts/docs/audit_builtins.py +++ b/scripts/docs/audit_builtins.py @@ -50,6 +50,9 @@ def _check_links(path: Path, errors: list[str]) -> None: for label, target in LINK_RE.findall(text): if target.startswith(("http://", "https://", "#", "mailto:")): continue + # Drop the in-page anchor before checking the filesystem target + # ([text](page.md#section) links to a heading inside page.md). + target = target.split("#", 1)[0] if target.startswith("/"): # Absolute path from site root — verify under the repo's docs/. abs_target = (REPO / "docs" / target.lstrip("/")).resolve() diff --git a/scripts/docs/builtin_registry.json b/scripts/docs/builtin_registry.json index 23926fbe0c..ffb7021d8d 100644 --- a/scripts/docs/builtin_registry.json +++ b/scripts/docs/builtin_registry.json @@ -9,6 +9,7 @@ }, "eval_only": false, "in_catalog": false, + "is_extension": false, "is_internal": true, "lowering": { "checker_file": null, @@ -91,6 +92,7 @@ }, "eval_only": false, "in_catalog": false, + "is_extension": false, "is_internal": true, "lowering": { "checker_file": null, @@ -173,6 +175,7 @@ }, "eval_only": false, "in_catalog": false, + "is_extension": false, "is_internal": true, "lowering": { "checker_file": null, @@ -216,6 +219,7 @@ }, "eval_only": false, "in_catalog": false, + "is_extension": false, "is_internal": true, "lowering": { "checker_file": null, @@ -259,6 +263,7 @@ }, "eval_only": false, "in_catalog": false, + "is_extension": false, "is_internal": true, "lowering": { "checker_file": null, @@ -301,6 +306,7 @@ }, "eval_only": false, "in_catalog": false, + "is_extension": false, "is_internal": true, "lowering": { "checker_file": null, @@ -343,6 +349,7 @@ }, "eval_only": false, "in_catalog": false, + "is_extension": false, "is_internal": true, "lowering": { "checker_file": null, @@ -385,6 +392,7 @@ }, "eval_only": false, "in_catalog": false, + "is_extension": false, "is_internal": true, "lowering": { "checker_file": null, @@ -427,6 +435,7 @@ }, "eval_only": false, "in_catalog": false, + "is_extension": false, "is_internal": true, "lowering": { "checker_file": null, @@ -469,6 +478,7 @@ }, "eval_only": false, "in_catalog": false, + "is_extension": false, "is_internal": true, "lowering": { "checker_file": null, @@ -512,6 +522,7 @@ }, "eval_only": false, "in_catalog": false, + "is_extension": false, "is_internal": true, "lowering": { "checker_file": null, @@ -555,6 +566,7 @@ }, "eval_only": false, "in_catalog": false, + "is_extension": false, "is_internal": true, "lowering": { "checker_file": null, @@ -605,6 +617,7 @@ }, "eval_only": false, "in_catalog": false, + "is_extension": false, "is_internal": true, "lowering": { "checker_file": null, @@ -656,6 +669,7 @@ }, "eval_only": false, "in_catalog": false, + "is_extension": false, "is_internal": true, "lowering": { "checker_file": null, @@ -705,6 +719,7 @@ }, "eval_only": false, "in_catalog": false, + "is_extension": false, "is_internal": true, "lowering": { "checker_file": null, @@ -754,6 +769,7 @@ }, "eval_only": false, "in_catalog": false, + "is_extension": false, "is_internal": true, "lowering": { "checker_file": null, @@ -797,6 +813,7 @@ }, "eval_only": false, "in_catalog": false, + "is_extension": false, "is_internal": true, "lowering": { "checker_file": null, @@ -846,6 +863,7 @@ }, "eval_only": false, "in_catalog": false, + "is_extension": false, "is_internal": true, "lowering": { "checker_file": null, @@ -885,6 +903,153 @@ "slug": "__elephc_phar_sign_openssl", "sub_area": "IO" }, + { + "area": "Pointer", + "canonical_name": "__elephc_ptr_is_null", + "description": "Internal prelude alias of ptr_is_null.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, + "in_catalog": false, + "is_extension": false, + "is_internal": true, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/codegen/lower_inst/builtins/pointers.rs", + "codegen_function": "lower_ptr_is_null", + "codegen_line": 51, + "notes": [ + "Lowers `ptr_is_null(pointer)` by comparing the raw pointer address to zero." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/pointers/__elephc_ptr_is_null.rs", + "sig_line": null + }, + "name": "__elephc_ptr_is_null", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false, + "type": "mixed" + } + ], + "return_type": "bool", + "variadic": null + }, + "slug": "__elephc_ptr_is_null", + "sub_area": "Pointer" + }, + { + "area": "Pointer", + "canonical_name": "__elephc_ptr_read_string", + "description": "Internal prelude alias of ptr_read_string.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, + "in_catalog": false, + "is_extension": false, + "is_internal": true, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/codegen/lower_inst/builtins/pointers.rs", + "codegen_function": "lower_ptr_read_string", + "codegen_line": 129, + "notes": [ + "Lowers `ptr_read_string(pointer, length)` by copying raw bytes into an owned PHP string." + ], + "runtime_helpers": [ + "__rt_ptr_read_string" + ], + "sig_arm": null, + "sig_file": "src/builtins/pointers/__elephc_ptr_read_string.rs", + "sig_line": null + }, + "name": "__elephc_ptr_read_string", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false, + "type": "mixed" + }, + { + "by_ref": false, + "default": null, + "name": "length", + "optional": false, + "type": "mixed" + } + ], + "return_type": "string", + "variadic": null + }, + "slug": "__elephc_ptr_read_string", + "sub_area": "Pointer" + }, + { + "area": "Pointer", + "canonical_name": "__elephc_ptr_write_string", + "description": "Internal prelude alias of ptr_write_string.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, + "in_catalog": false, + "is_extension": false, + "is_internal": true, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/codegen/lower_inst/builtins/pointers.rs", + "codegen_function": "lower_ptr_write_string", + "codegen_line": 166, + "notes": [ + "Lowers `ptr_write_string(pointer, string)` by copying PHP string bytes into raw memory." + ], + "runtime_helpers": [ + "__rt_ptr_write_string" + ], + "sig_arm": null, + "sig_file": "src/builtins/pointers/__elephc_ptr_write_string.rs", + "sig_line": null + }, + "name": "__elephc_ptr_write_string", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false, + "type": "mixed" + }, + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false, + "type": "mixed" + } + ], + "return_type": "int", + "variadic": null + }, + "slug": "__elephc_ptr_write_string", + "sub_area": "Pointer" + }, { "area": "Misc", "canonical_name": "__elephc_strtotime_raw", @@ -895,6 +1060,7 @@ }, "eval_only": false, "in_catalog": false, + "is_extension": false, "is_internal": true, "lowering": { "checker_file": null, @@ -963,6 +1129,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -1023,6 +1190,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -1081,6 +1249,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -1125,6 +1294,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -1177,6 +1347,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -1250,6 +1421,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -1321,6 +1493,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -1392,6 +1565,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -1457,6 +1631,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -1505,6 +1680,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -1565,6 +1741,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -1638,6 +1815,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -1716,6 +1894,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -1793,6 +1972,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -1852,6 +2032,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -1919,6 +2100,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -1977,6 +2159,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -2024,6 +2207,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -2087,6 +2271,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -2131,6 +2316,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -2201,6 +2387,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -2252,6 +2439,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -2297,6 +2485,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -2358,6 +2547,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -2422,6 +2612,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -2480,6 +2671,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -2517,6 +2709,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -2555,6 +2748,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -2635,6 +2829,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -2706,6 +2901,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -2767,6 +2963,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -2826,6 +3023,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -2884,6 +3082,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -2957,6 +3156,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -3015,6 +3215,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -3068,6 +3269,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -3142,6 +3344,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -3212,6 +3415,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -3283,6 +3487,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -3353,6 +3558,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -3442,6 +3648,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -3514,6 +3721,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -3559,6 +3767,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -3615,6 +3824,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -3687,6 +3897,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -3747,6 +3958,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -3805,6 +4017,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -3868,6 +4081,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -3919,6 +4133,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -3987,6 +4202,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -4051,6 +4267,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -4108,6 +4325,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -4173,6 +4391,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -4237,6 +4456,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -4302,6 +4522,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -4362,6 +4583,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -4428,6 +4650,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -4493,6 +4716,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -4553,13 +4777,14 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_boolval", - "codegen_line": 1176, + "codegen_line": 1174, "notes": [ "Lowers `boolval()` using the same concrete scalar PHP truthiness rules as `IsTruthy`." ], @@ -4590,7 +4815,7 @@ { "area": "Buffer", "canonical_name": "buffer_free", - "description": "Lowers `buffer_free()` through the direct buffer opcode helper.", + "description": "Frees a buffer and nulls the local variable that held it.", "eval": { "area": "raw_memory", "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_free.rs", @@ -4613,19 +4838,20 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/buffers.rs", "codegen_function": "lower_buffer_free", - "codegen_line": 24, + "codegen_line": 25, "notes": [ "Lowers `buffer_free()` through the direct buffer opcode helper." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/pointers/buffer_free.rs", "sig_line": null }, "name": "buffer_free", @@ -4639,7 +4865,7 @@ "type": "buffer" } ], - "return_type": "mixed", + "return_type": "void", "variadic": null }, "slug": "buffer_free", @@ -4648,7 +4874,7 @@ { "area": "Buffer", "canonical_name": "buffer_len", - "description": "Lowers `buffer_len()` through the direct buffer opcode helper.", + "description": "Returns the logical element count of a buffer.", "eval": { "area": "raw_memory", "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_len.rs", @@ -4671,19 +4897,20 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/buffers.rs", "codegen_function": "lower_buffer_len", - "codegen_line": 19, + "codegen_line": 20, "notes": [ "Lowers `buffer_len()` through the direct buffer opcode helper." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/pointers/buffer_len.rs", "sig_line": null }, "name": "buffer_len", @@ -4729,6 +4956,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": true, "is_internal": false, "lowering": { "checker_file": null, @@ -4785,6 +5013,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -4852,6 +5081,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -4920,6 +5150,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -4978,6 +5209,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -5054,6 +5286,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -5138,6 +5371,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -5211,6 +5445,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -5282,6 +5517,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -5353,6 +5589,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -5420,6 +5657,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -5492,6 +5730,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -5576,6 +5815,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -5654,6 +5894,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": true, "is_internal": false, "lowering": { "checker_file": null, @@ -5719,6 +5960,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": true, "is_internal": false, "lowering": { "checker_file": null, @@ -5783,13 +6025,14 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_class_like_exists", - "codegen_line": 583, + "codegen_line": 581, "notes": [ "Lowers AOT class/interface/enum existence checks for literal or dynamic string names." ], @@ -5848,6 +6091,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": true, "is_internal": false, "lowering": { "checker_file": null, @@ -5912,6 +6156,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -5983,6 +6228,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -6054,6 +6300,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -6125,6 +6372,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -6192,6 +6440,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -6261,6 +6510,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -6331,6 +6581,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -6389,6 +6640,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -6453,13 +6705,14 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_count", - "codegen_line": 1025, + "codegen_line": 1023, "notes": [ "Lowers `count(array)` for concrete array values by reading the runtime length header.", "Called from `crate::builtins::array::count` (the registry home) via a thin wrapper.", @@ -6524,6 +6777,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -6585,6 +6839,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -6643,6 +6898,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -6701,6 +6957,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -6759,6 +7016,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -6823,6 +7081,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -6884,6 +7143,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -6939,6 +7199,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -7011,13 +7272,14 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_define", - "codegen_line": 372, + "codegen_line": 370, "notes": [ "Lowers `define(\"NAME\", value)` with the duplicate-name runtime guard." ], @@ -7076,13 +7338,14 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_defined", - "codegen_line": 551, + "codegen_line": 549, "notes": [ "Lowers `defined(\"NAME\")` for compile-time string constant names." ], @@ -7134,6 +7397,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -7192,6 +7456,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -7254,6 +7519,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -7322,6 +7588,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -7382,6 +7649,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -7442,13 +7710,14 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_empty", - "codegen_line": 1218, + "codegen_line": 1216, "notes": [ "Lowers `empty()` for concrete scalar and array-like operands." ], @@ -7508,13 +7777,14 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_class_like_exists", - "codegen_line": 583, + "codegen_line": 581, "notes": [ "Lowers AOT class/interface/enum existence checks for literal or dynamic string names." ], @@ -7573,6 +7843,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -7631,6 +7902,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -7687,6 +7959,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -7757,6 +8030,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -7832,6 +8106,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -7890,6 +8165,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -7956,6 +8232,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -8021,6 +8298,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -8082,6 +8360,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -8142,6 +8421,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -8215,6 +8495,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -8290,6 +8571,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -8351,6 +8633,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -8412,6 +8695,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -8473,6 +8757,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -8540,6 +8825,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -8608,6 +8894,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -8671,6 +8958,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -8734,6 +9022,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -8797,6 +9086,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -8860,6 +9150,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -8924,6 +9215,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -8986,6 +9278,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -9049,6 +9342,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -9112,6 +9406,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -9174,13 +9469,14 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_floatval", - "codegen_line": 1142, + "codegen_line": 1140, "notes": [ "Lowers `floatval()` for concrete scalar operands." ], @@ -9246,6 +9542,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -9318,6 +9615,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -9382,6 +9680,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -9459,6 +9758,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -9549,6 +9849,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -9630,6 +9931,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -9697,6 +9999,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -9782,6 +10085,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -9869,6 +10173,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -9942,6 +10247,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -10022,6 +10328,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -10117,6 +10424,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -10203,6 +10511,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -10263,6 +10572,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -10324,6 +10634,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -10390,6 +10701,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -10455,13 +10767,14 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_function_exists", - "codegen_line": 566, + "codegen_line": 564, "notes": [ "Lowers `function_exists(\"name\")` for compile-time string names.", "Recognizes user functions, externs, catalog builtins, and the date/time procedural aliases", @@ -10524,6 +10837,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -10584,6 +10898,7 @@ }, "eval_only": true, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -10632,6 +10947,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -10690,6 +11006,7 @@ }, "eval_only": true, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -10746,6 +11063,7 @@ }, "eval_only": true, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -10795,6 +11113,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -10838,6 +11157,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -10881,6 +11201,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -10931,6 +11252,7 @@ }, "eval_only": true, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -10987,6 +11309,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -11045,6 +11368,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -11103,6 +11427,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -11154,6 +11479,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -11207,6 +11533,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -11272,6 +11599,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -11332,6 +11660,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -11393,6 +11722,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -11447,6 +11777,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -11501,6 +11832,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -11561,6 +11893,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -11627,6 +11960,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -11700,6 +12034,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -11767,13 +12102,14 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_gettype", - "codegen_line": 419, + "codegen_line": 417, "notes": [ "Lowers `gettype(value)` for statically concrete PHP types." ], @@ -11825,6 +12161,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -11891,6 +12228,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -11991,6 +12329,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -12090,6 +12429,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -12157,6 +12497,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -12228,6 +12569,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -12299,6 +12641,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -12370,6 +12713,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -12449,6 +12793,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -12516,6 +12861,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -12569,6 +12915,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -12636,6 +12983,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -12717,6 +13065,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -12795,6 +13144,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -12880,6 +13230,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -12974,6 +13325,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -13054,6 +13406,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -13133,6 +13486,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -13212,6 +13566,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -13272,6 +13627,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -13337,6 +13693,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -13409,6 +13766,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -13502,6 +13860,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -13583,6 +13942,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -13653,6 +14013,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -13724,6 +14085,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -13801,6 +14163,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -13873,6 +14236,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -13933,6 +14297,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -13999,6 +14364,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -14070,13 +14436,14 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_class_like_exists", - "codegen_line": 583, + "codegen_line": 581, "notes": [ "Lowers AOT class/interface/enum existence checks for literal or dynamic string names." ], @@ -14135,13 +14502,14 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_intval", - "codegen_line": 1109, + "codegen_line": 1107, "notes": [ "Lowers `intval()` for concrete scalar operands." ], @@ -14196,6 +14564,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -14269,6 +14638,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -14341,13 +14711,14 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_array", - "codegen_line": 1603, + "codegen_line": 1601, "notes": [ "Lowers `is_array()`: true for statically-known arrays/hashes, or a boxed Mixed/Union value", "whose runtime tag is an indexed (4) or associative (5) array. An `iterable`-typed value is", @@ -14401,13 +14772,14 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_static_type_predicate", - "codegen_line": 1336, + "codegen_line": 1334, "notes": [ "Lowers a static `is_*` predicate for concrete non-Mixed values." ], @@ -14471,13 +14843,14 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_callable", - "codegen_line": 712, + "codegen_line": 710, "notes": [ "Lowers `is_callable(value)` through static lookup or runtime callable-shape helpers." ], @@ -14532,6 +14905,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -14594,6 +14968,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -14657,6 +15032,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -14719,6 +15095,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -14777,13 +15154,14 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_static_type_predicate", - "codegen_line": 1336, + "codegen_line": 1334, "notes": [ "Lowers a static `is_*` predicate for concrete non-Mixed values." ], @@ -14835,6 +15213,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -14893,13 +15272,14 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_static_type_predicate", - "codegen_line": 1336, + "codegen_line": 1334, "notes": [ "Lowers a static `is_*` predicate for concrete non-Mixed values." ], @@ -14951,13 +15331,14 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_iterable", - "codegen_line": 1394, + "codegen_line": 1392, "notes": [ "Lowers `is_iterable()` for concrete values and boxed Mixed payloads." ], @@ -15009,6 +15390,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -15072,6 +15454,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -15130,13 +15513,14 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_null_builtin", - "codegen_line": 1593, + "codegen_line": 1591, "notes": [ "Lowers `is_null()` for concrete scalar values and boxed Mixed payloads." ], @@ -15188,6 +15572,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -15246,13 +15631,14 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_object", - "codegen_line": 1618, + "codegen_line": 1616, "notes": [ "Lowers `is_object()`: true for statically-known objects, or a boxed Mixed/Union value whose", "runtime tag is an object (6)." @@ -15305,6 +15691,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -15367,6 +15754,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -15425,13 +15813,14 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_scalar", - "codegen_line": 1634, + "codegen_line": 1632, "notes": [ "Lowers `is_scalar()`: true for int/float/string/bool, a non-null tagged scalar, or a boxed", "Mixed/Union value whose runtime tag is int (0), string (1), float (2), or bool (3). Null,", @@ -15485,13 +15874,14 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_static_type_predicate", - "codegen_line": 1336, + "codegen_line": 1334, "notes": [ "Lowers a static `is_*` predicate for concrete non-Mixed values." ], @@ -15555,6 +15945,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -15627,6 +16018,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -15689,6 +16081,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -15751,6 +16144,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -15821,6 +16215,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -15893,6 +16288,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -15957,6 +16353,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -16040,6 +16437,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -16133,6 +16531,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -16198,6 +16597,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -16243,6 +16643,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -16308,6 +16709,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -16381,6 +16783,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -16442,6 +16845,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -16505,6 +16909,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -16571,6 +16976,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -16644,6 +17050,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -16717,6 +17124,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -16787,6 +17195,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -16856,6 +17265,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -16935,6 +17345,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -17000,6 +17411,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -17058,6 +17470,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -17116,6 +17529,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -17177,6 +17591,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -17244,6 +17659,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -17309,6 +17725,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -17379,6 +17796,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -17461,6 +17879,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -17535,6 +17954,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -17604,6 +18024,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -17673,6 +18094,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -17731,6 +18153,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -17825,6 +18248,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -17928,6 +18352,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -17994,6 +18419,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -18053,6 +18479,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -18120,6 +18547,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -18198,6 +18626,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -18279,6 +18708,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -18341,6 +18771,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -18399,6 +18830,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -18466,6 +18898,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -18533,6 +18966,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -18616,6 +19050,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -18702,6 +19137,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -18755,13 +19191,14 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_phpversion", - "codegen_line": 541, + "codegen_line": 539, "notes": [ "Lowers `phpversion()` as the compiler package version string." ], @@ -18798,6 +19235,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -18854,6 +19292,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -18927,6 +19366,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -19010,6 +19450,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -19103,6 +19544,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -19183,6 +19625,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -19269,6 +19712,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -19361,6 +19805,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -19448,6 +19893,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -19524,6 +19970,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -19584,6 +20031,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": true, "is_internal": false, "lowering": { "checker_file": null, @@ -19642,6 +20090,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": true, "is_internal": false, "lowering": { "checker_file": null, @@ -19700,6 +20149,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": true, "is_internal": false, "lowering": { "checker_file": null, @@ -19751,6 +20201,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": true, "is_internal": false, "lowering": { "checker_file": null, @@ -19807,6 +20258,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": true, "is_internal": false, "lowering": { "checker_file": null, @@ -19872,6 +20324,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": true, "is_internal": false, "lowering": { "checker_file": null, @@ -19932,6 +20385,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": true, "is_internal": false, "lowering": { "checker_file": null, @@ -19992,6 +20446,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": true, "is_internal": false, "lowering": { "checker_file": null, @@ -20056,6 +20511,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": true, "is_internal": false, "lowering": { "checker_file": null, @@ -20129,6 +20585,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": true, "is_internal": false, "lowering": { "checker_file": null, @@ -20194,6 +20651,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": true, "is_internal": false, "lowering": { "checker_file": null, @@ -20258,6 +20716,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": true, "is_internal": false, "lowering": { "checker_file": null, @@ -20331,6 +20790,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": true, "is_internal": false, "lowering": { "checker_file": null, @@ -20404,6 +20864,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": true, "is_internal": false, "lowering": { "checker_file": null, @@ -20475,6 +20936,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": true, "is_internal": false, "lowering": { "checker_file": null, @@ -20542,6 +21004,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -20602,6 +21065,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -20666,6 +21130,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -20739,6 +21204,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -20810,6 +21276,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -20878,6 +21345,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -20938,6 +21406,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -20998,6 +21467,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -21061,6 +21531,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -21119,6 +21590,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -21179,6 +21651,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -21242,6 +21715,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -21295,6 +21769,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -21338,6 +21813,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -21394,6 +21870,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -21463,6 +21940,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -21521,6 +21999,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -21582,6 +22061,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -21652,6 +22132,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -21716,6 +22197,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -21788,6 +22270,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -21853,6 +22336,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -21898,6 +22382,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -21968,6 +22453,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -22039,6 +22525,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -22107,6 +22594,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -22164,6 +22652,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -22224,6 +22713,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -22282,6 +22772,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -22340,6 +22831,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -22399,6 +22891,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -22472,6 +22965,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -22537,6 +23031,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -22595,6 +23090,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -22646,6 +23142,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -22708,6 +23205,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -22780,6 +23278,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -22831,6 +23330,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -22883,6 +23383,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -22943,6 +23444,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -23003,6 +23505,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -23063,6 +23566,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -23127,6 +23631,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -23195,6 +23700,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -23263,6 +23769,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -23336,6 +23843,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -23421,6 +23929,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -23518,6 +24027,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -23605,6 +24115,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -23690,6 +24201,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -23775,6 +24287,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -23848,6 +24361,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -23921,6 +24435,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -23994,6 +24509,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -24067,6 +24583,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -24132,6 +24649,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -24198,6 +24716,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -24269,6 +24788,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -24340,6 +24860,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -24405,6 +24926,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -24463,6 +24985,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -24524,6 +25047,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -24582,6 +25106,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -24658,6 +25183,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -24743,6 +25269,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -24826,6 +25353,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -24923,6 +25451,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -25020,6 +25549,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -25105,6 +25635,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -25172,6 +25703,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -25244,6 +25776,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -25309,6 +25842,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -25371,6 +25905,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -25443,6 +25978,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -25496,6 +26032,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -25539,6 +26076,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -25589,6 +26127,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -25647,6 +26186,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -25707,6 +26247,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -25791,6 +26332,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -25883,6 +26425,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -25957,6 +26500,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -26028,6 +26572,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -26105,6 +26650,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -26183,6 +26729,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -26259,6 +26806,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -26333,6 +26881,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -26412,6 +26961,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -26497,6 +27047,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -26576,6 +27127,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -26667,6 +27219,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -26764,6 +27317,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -26843,6 +27397,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -26909,6 +27464,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -26976,6 +27532,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -27048,6 +27605,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -27122,6 +27680,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -27180,6 +27739,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -27240,6 +27800,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -27300,13 +27861,14 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_strlen", - "codegen_line": 1079, + "codegen_line": 1077, "notes": [ "Lowers `strlen()` by coercing string-like values and returning the byte length." ], @@ -27372,6 +27934,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -27444,6 +28007,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -27516,6 +28080,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -27600,6 +28165,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -27672,6 +28238,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -27738,6 +28305,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -27811,6 +28379,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -27883,6 +28452,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -27975,6 +28545,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -28063,6 +28634,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -28126,6 +28698,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -28178,6 +28751,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -28238,6 +28812,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -28296,6 +28871,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -28360,6 +28936,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -28422,6 +28999,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -28467,6 +29045,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -28533,6 +29112,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -28611,13 +29191,14 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_class_like_exists", - "codegen_line": 583, + "codegen_line": 581, "notes": [ "Lowers AOT class/interface/enum existence checks for literal or dynamic string names." ], @@ -28682,6 +29263,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -28752,6 +29334,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -28819,6 +29402,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -28885,6 +29469,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -28957,6 +29542,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -29024,6 +29610,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -29084,6 +29671,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -29130,6 +29718,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -29202,6 +29791,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -29260,6 +29850,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -29320,6 +29911,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -29380,6 +29972,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -29445,6 +30038,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -29512,6 +30106,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -29583,6 +30178,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -29664,6 +30260,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -29737,6 +30334,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -29822,6 +30420,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, @@ -29888,6 +30487,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": true, "is_internal": false, "lowering": { "checker_file": null, @@ -29932,6 +30532,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": true, "is_internal": false, "lowering": { "checker_file": null, @@ -29985,6 +30586,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": true, "is_internal": false, "lowering": { "checker_file": null, @@ -30030,6 +30632,7 @@ }, "eval_only": false, "in_catalog": true, + "is_extension": true, "is_internal": false, "lowering": { "checker_file": null, diff --git a/scripts/docs/elephc_builtins/extract.py b/scripts/docs/elephc_builtins/extract.py index 917d5705fa..f1a65313ca 100644 --- a/scripts/docs/elephc_builtins/extract.py +++ b/scripts/docs/elephc_builtins/extract.py @@ -17,10 +17,11 @@ 3. optional type-precision refinements for non-scalar params/returns that the registry represents coarsely as ``Mixed`` (``PARAM_TYPES`` / ``RETURN_TYPE_OVERRIDES``). -The 8 PHP language constructs that intentionally stay checker-resident -(``isset``/``unset``/``empty``/``exit``/``die``/``buffer_len``/``buffer_free``/ -``buffer_new``) are not in the registry; they are added from a small hand-curated -table so their documentation pages are preserved. +The 6 names that intentionally stay outside the registry +(``isset``/``unset``/``empty``/``exit``/``die`` language constructs, plus the +catalog-name-only ``buffer_new`` whose call form is dedicated syntax) are added +from a small hand-curated table so their documentation pages are preserved. +``buffer_len``/``buffer_free`` live in the ``builtin!`` registry. The output is a list of :class:`registry.Builtin` written to a JSON file in ``scripts/docs/builtin_registry.json``. @@ -447,22 +448,9 @@ def _render_default(value, optional: bool) -> Optional[str]: "description": "", "emitter_fn": None, }, - "buffer_len": { - "params": [("buffer", "buffer", False, None, False)], - "variadic": None, - "return_type": "int", - "area": ("Buffer", "Buffer"), - "description": "Lowers `buffer_len()` through the direct buffer opcode helper.", - "emitter_fn": "lower_buffer_len", - }, - "buffer_free": { - "params": [("buffer", "buffer", False, None, False)], - "variadic": None, - "return_type": "mixed", - "area": ("Buffer", "Buffer"), - "description": "Lowers `buffer_free()` through the direct buffer opcode helper.", - "emitter_fn": "lower_buffer_free", - }, + # `buffer_len` / `buffer_free` moved to the single-source `builtin!` registry + # (src/builtins/pointers/); only the catalog-name-only `buffer_new` remains + # hand-described here because its call form is dedicated syntax. "buffer_new": { "params": [("length", "int", False, None, False)], "variadic": None, @@ -470,6 +458,7 @@ def _render_default(value, optional: bool) -> Optional[str]: "area": ("Misc", "Misc"), "description": "", "emitter_fn": None, + "extension": True, }, } @@ -534,6 +523,7 @@ def _eval_only_builtin(entry: dict) -> Builtin: description=description, eval_support=eval_support, eval_only=True, + is_extension=bool(entry.get("extension")), ) @@ -648,6 +638,7 @@ def resolve_check_body(fn_name: str) -> str: lowering=lowering, description=description, eval_support=entry.get("eval"), + is_extension=bool(entry.get("extension")), ) ) @@ -682,6 +673,7 @@ def resolve_check_body(fn_name: str) -> str: lowering=lowering, description=description, eval_support=resident_eval.get(canonical), + is_extension=bool(spec.get("extension")), ) ) @@ -723,6 +715,7 @@ def _builtin_to_dict(b: Builtin) -> dict: "sub_area": b.sub_area, "in_catalog": b.in_catalog, "is_internal": b.is_internal, + "is_extension": b.is_extension, "description": b.description, "sig": { "params": [ diff --git a/scripts/docs/elephc_builtins/registry.py b/scripts/docs/elephc_builtins/registry.py index 6bd7e82a01..bf66322cca 100644 --- a/scripts/docs/elephc_builtins/registry.py +++ b/scripts/docs/elephc_builtins/registry.py @@ -1206,6 +1206,9 @@ class Builtin: eval_support: Optional[dict] = None # True when only the eval interpreter exposes this builtin (no AOT support). eval_only: bool = False + # True for elephc extensions with no PHP equivalent (ptr_*, buffer_*, + # class_attribute_*); `--strict-php` hides them from user programs. + is_extension: bool = False def slug(name: str) -> str: diff --git a/scripts/docs/elephc_builtins/render.py b/scripts/docs/elephc_builtins/render.py index 3fc5004d99..8f76f1c1f1 100644 --- a/scripts/docs/elephc_builtins/render.py +++ b/scripts/docs/elephc_builtins/render.py @@ -239,6 +239,13 @@ def _availability_section(b: dict) -> str: lines.append( "- **`eval()` (magician interpreter)**: not available inside eval'd code." ) + if b.get("is_extension"): + lines.append( + "- **Strict PHP mode**: hidden — this builtin is an elephc extension " + "with no PHP equivalent, so programs compiled with " + "[`--strict-php`](../../../compiling/cli-reference.md#strict-php-mode) " + "treat the name as nonexistent, in compiled code and inside eval'd code." + ) return "\n".join(lines) diff --git a/src/autoload/mod.rs b/src/autoload/mod.rs index e358a3934d..55be9275c5 100644 --- a/src/autoload/mod.rs +++ b/src/autoload/mod.rs @@ -210,6 +210,9 @@ fn load_autoloaded_file(path: &Path, base_dir: &Path) -> Result = crate::name_resolver::resolve(resolved)?; diff --git a/src/builtin_metadata.rs b/src/builtin_metadata.rs index fbab246f2d..ef0bbe94ef 100644 --- a/src/builtin_metadata.rs +++ b/src/builtin_metadata.rs @@ -26,14 +26,42 @@ pub struct BuiltinSignatureMetadata { } /// Returns the compiler's PHP-visible builtin names. +/// +/// Reads the unfiltered catalog snapshot — never the strict-PHP-filtered view — +/// so the memoized result is independent of the thread's strict-mode state. pub fn php_visible_builtin_names() -> &'static [&'static str] { static NAMES: std::sync::OnceLock<&'static [&'static str]> = std::sync::OnceLock::new(); NAMES.get_or_init(|| { - let names = crate::types::checker::builtins::supported_builtin_function_names(); + let names = + crate::types::checker::builtins::catalog::all_supported_builtin_function_names(); Box::leak(names.into_boxed_slice()) }) } +/// Returns the compiler's PHP-visible extension builtin names (elephc-only +/// builtins hidden by `--strict-php`), in stable sorted order. Reads the +/// registry's `extension` flags directly — never the strict-filtered catalog — +/// so the snapshot is independent of the thread's strict-mode state. Includes +/// the catalog-name-only `buffer_new` entry. +pub fn extension_builtin_names() -> &'static [&'static str] { + static NAMES: std::sync::OnceLock> = std::sync::OnceLock::new(); + NAMES + .get_or_init(|| { + let mut names: Vec<&'static str> = vec!["buffer_new"]; + for name in crate::builtins::registry::names() { + let Some(def) = crate::builtins::registry::lookup(name) else { + continue; + }; + if def.spec.extension && !def.spec.internal { + names.push(def.name); + } + } + names.sort_unstable(); + names + }) + .as_slice() +} + /// Returns comparison metadata for one builtin signature, when the compiler tracks it. pub fn builtin_signature_metadata(name: &str) -> Option { let canonical = crate::names::php_symbol_key(name.trim_start_matches('\\')); diff --git a/src/builtins/docs.rs b/src/builtins/docs.rs index d8f14becf7..c609f91a55 100644 --- a/src/builtins/docs.rs +++ b/src/builtins/docs.rs @@ -128,6 +128,7 @@ fn build_json(include_internal: bool) -> Value { "name": spec.name, "area": area_str(spec.area), "internal": spec.internal, + "extension": spec.extension, "params": params, "variadic": spec.variadic, "returns": type_spec_str(&spec.returns), diff --git a/src/builtins/macros.rs b/src/builtins/macros.rs index ca8a80fd70..44bab50316 100644 --- a/src/builtins/macros.rs +++ b/src/builtins/macros.rs @@ -21,7 +21,7 @@ //! name, area, params, variadic?, min_args?, max_args?, arity_error?, returns, //! returns_fresh_storage?, returns_independent_storage?, by_ref_return?, check?, //! lazy_check?, lower, summary, examples?, php_manual?, -//! deprecation?, internal? +//! deprecation?, extension?, internal? //! //! Example: //! ```ignore @@ -42,7 +42,10 @@ /// `name`, `area`, `params`, `variadic`?, `min_args`?, `max_args`?, `arity_error`?, /// `returns`, `returns_fresh_storage`?, `returns_independent_storage`?, /// `by_ref_return`?, `check`?, `lazy_check`?, `lower`, `summary`, `examples`?, -/// `php_manual`?, `deprecation`?, `internal`? +/// `php_manual`?, `deprecation`?, `extension`?, `internal`? +/// +/// `extension` (optional `bool`, default `false`) marks the builtin as an elephc +/// extension with no PHP equivalent; `--strict-php` hides it from user programs. /// /// `max_args` (optional `usize`) caps the maximum argument count enforced by the /// registry's `check_arity` only; it does not affect `function_sig` or the parity gate. @@ -92,6 +95,7 @@ macro_rules! builtin { $(examples: $examples:expr,)? $(php_manual: $php_manual:expr,)? $(deprecation: $deprecation:expr,)? + $(extension: $extension:expr,)? $(internal: $internal:expr)? $(,)? ) => { @@ -119,6 +123,7 @@ macro_rules! builtin { examples: builtin!(@opt_examples $($examples)?), php_manual: builtin!(@opt_str $($php_manual)?), deprecation: builtin!(@opt_str $($deprecation)?), + extension: builtin!(@opt_bool $($extension)?), internal: builtin!(@opt_bool $($internal)?), } } diff --git a/src/builtins/parity_tests.rs b/src/builtins/parity_tests.rs index 1871a0b098..462b8921d0 100644 --- a/src/builtins/parity_tests.rs +++ b/src/builtins/parity_tests.rs @@ -131,3 +131,115 @@ fn derived_signatures_match_legacy() { assert_behavior_fields_match(name, &derived, &legacy); } } + +/// The exact set of PHP-visible builtins that are elephc extensions (no PHP +/// equivalent), pinned so that adding or reclassifying a builtin is a conscious, +/// reviewable decision. `--strict-php` hides exactly this set (plus the +/// `buffer_new` catalog-name-only entry) from user programs. +const EXPECTED_EXTENSION_BUILTINS: &[&str] = &[ + "buffer_free", + "buffer_len", + "class_attribute_args", + "class_attribute_names", + "class_get_attributes", + "ptr", + "ptr_get", + "ptr_is_null", + "ptr_null", + "ptr_offset", + "ptr_read16", + "ptr_read32", + "ptr_read8", + "ptr_read_string", + "ptr_set", + "ptr_sizeof", + "ptr_write16", + "ptr_write32", + "ptr_write8", + "ptr_write_string", + "zval_free", + "zval_pack", + "zval_type", + "zval_unpack", +]; + +/// Verifies no injected compiler prelude calls a PHP-visible extension builtin. +/// +/// `--strict-php` hides extension builtins at the catalog level with no notion +/// of code origin, so a prelude calling one (instead of its `internal: true` +/// `__elephc_*` alias) would break strict-mode compiles of programs that +/// trigger that prelude's injection. Scans every prelude PHP source for +/// `(` call sites; bare mentions inside comments are tolerated. +#[test] +fn preludes_never_call_php_visible_extension_builtins() { + let mut extension_names: Vec = vec!["buffer_new".to_string()]; + for name in registry::names() { + let def = registry::lookup(name).expect("names() yields registered builtins"); + if def.spec.extension && !def.spec.internal { + extension_names.push(def.name.to_string()); + } + } + + let prelude_sources: &[(&str, &str)] = &[ + ("pdo_prelude", crate::pdo_prelude::PDO_PRELUDE_SRC), + ("tz_prelude", crate::tz_prelude::TZ_PRELUDE_SRC), + ("list_id_prelude", crate::list_id_prelude::LIST_ID_PRELUDE_TEMPLATE), + ("var_export_prelude", crate::var_export_prelude::VAR_EXPORT_PRELUDE_SRC), + ("image_prelude", crate::image_prelude::IMAGE_PRELUDE_SRC), + ("web_prelude", crate::web_prelude::WEB_PRELUDE_SRC), + ("web_prelude(wrap)", crate::web_prelude::WEB_WRAP_SRC), + ]; + + let mut violations: Vec = Vec::new(); + for (prelude, source) in prelude_sources { + for name in &extension_names { + if source_calls_function(source, name) { + violations.push(format!("{prelude} calls {name}()")); + } + } + } + assert!( + violations.is_empty(), + "preludes must call `__elephc_*` internal aliases, not PHP-visible extension builtins:\n{}", + violations.join("\n"), + ); +} + +/// Returns true when `source` contains a plain function-call site `name(`. +/// +/// A match is a call site only when the preceding character is not part of a +/// longer identifier (`elephc_pdo_column_data_ptr(`), a variable (`$ptr(`), or +/// a method/static access (`->ptr(`, `::ptr(`), so extern helpers whose names +/// merely end with a builtin name do not count. +fn source_calls_function(source: &str, name: &str) -> bool { + let needle = format!("{name}("); + source.match_indices(&needle).any(|(index, _)| { + match source[..index].chars().next_back() { + None => true, + Some(prev) => { + !prev.is_ascii_alphanumeric() && !matches!(prev, '_' | '$' | '>' | ':') + } + } + }) +} + +/// Verifies the registry's PHP-visible `extension: true` set matches the pinned +/// list exactly, in both directions: no extension builtin missing the flag, no +/// PHP builtin carrying it by mistake. Internal builtins are skipped: they are +/// not PHP-visible, strict mode never hides them, and cfg(test) probes may +/// combine `internal` with `extension` to exercise the macro. +#[test] +fn extension_builtin_set_is_pinned() { + let mut tagged: Vec<&str> = Vec::new(); + for name in registry::names() { + let def = registry::lookup(name).expect("names() yields registered builtins"); + if def.spec.extension && !def.spec.internal { + tagged.push(def.name); + } + } + tagged.sort_unstable(); + assert_eq!( + tagged, EXPECTED_EXTENSION_BUILTINS, + "extension builtin set drifted from the pinned list", + ); +} diff --git a/src/builtins/pointers/__elephc_ptr_is_null.rs b/src/builtins/pointers/__elephc_ptr_is_null.rs new file mode 100644 index 0000000000..bc86c30964 --- /dev/null +++ b/src/builtins/pointers/__elephc_ptr_is_null.rs @@ -0,0 +1,34 @@ +//! Purpose: +//! Home of the internal `__elephc_ptr_is_null` builtin: the compiler-prelude +//! alias of `ptr_is_null`, sharing its check hook and lowering. +//! +//! Called from: +//! - Injected prelude PHP sources (`src/image_prelude.rs`) through the builtin +//! registry. +//! +//! Key details: +//! - `internal: true`: never PHP-visible, so `--strict-php` (which hides +//! PHP-visible extension builtins from user programs) does not affect it and +//! prelude-injected code keeps compiling in strict mode. +//! - Delegates `check` and the lowering emitter to the `ptr_is_null` home so the +//! two names cannot drift. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "__elephc_ptr_is_null", + area: Pointers, + params: [pointer: Mixed], + returns: Bool, + check: crate::builtins::pointers::ptr_is_null::check, + lower: lower, + summary: "Internal prelude alias of ptr_is_null.", + internal: true, +} + +/// Lowers an `__elephc_ptr_is_null` call through the shared pointer emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::pointers::lower_ptr_is_null(ctx, inst) +} diff --git a/src/builtins/pointers/__elephc_ptr_read_string.rs b/src/builtins/pointers/__elephc_ptr_read_string.rs new file mode 100644 index 0000000000..f6e39a3d7f --- /dev/null +++ b/src/builtins/pointers/__elephc_ptr_read_string.rs @@ -0,0 +1,34 @@ +//! Purpose: +//! Home of the internal `__elephc_ptr_read_string` builtin: the compiler-prelude +//! alias of `ptr_read_string`, sharing its check hook and lowering. +//! +//! Called from: +//! - Injected prelude PHP sources (`src/image_prelude.rs`, `src/web_prelude.rs`) +//! through the builtin registry. +//! +//! Key details: +//! - `internal: true`: never PHP-visible, so `--strict-php` (which hides +//! PHP-visible extension builtins from user programs) does not affect it and +//! prelude-injected code keeps compiling in strict mode. +//! - Delegates `check` and the lowering emitter to the `ptr_read_string` home so +//! the two names cannot drift. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "__elephc_ptr_read_string", + area: Pointers, + params: [pointer: Mixed, length: Mixed], + returns: Str, + check: crate::builtins::pointers::ptr_read_string::check, + lower: lower, + summary: "Internal prelude alias of ptr_read_string.", + internal: true, +} + +/// Lowers an `__elephc_ptr_read_string` call through the shared pointer emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::pointers::lower_ptr_read_string(ctx, inst) +} diff --git a/src/builtins/pointers/__elephc_ptr_write_string.rs b/src/builtins/pointers/__elephc_ptr_write_string.rs new file mode 100644 index 0000000000..2dbb5b0d14 --- /dev/null +++ b/src/builtins/pointers/__elephc_ptr_write_string.rs @@ -0,0 +1,34 @@ +//! Purpose: +//! Home of the internal `__elephc_ptr_write_string` builtin: the compiler-prelude +//! alias of `ptr_write_string`, sharing its check hook and lowering. +//! +//! Called from: +//! - Injected prelude PHP sources (`src/image_prelude.rs`) through the builtin +//! registry. +//! +//! Key details: +//! - `internal: true`: never PHP-visible, so `--strict-php` (which hides +//! PHP-visible extension builtins from user programs) does not affect it and +//! prelude-injected code keeps compiling in strict mode. +//! - Delegates `check` and the lowering emitter to the `ptr_write_string` home so +//! the two names cannot drift. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "__elephc_ptr_write_string", + area: Pointers, + params: [pointer: Mixed, string: Mixed], + returns: Int, + check: crate::builtins::pointers::ptr_write_string::check, + lower: lower, + summary: "Internal prelude alias of ptr_write_string.", + internal: true, +} + +/// Lowers an `__elephc_ptr_write_string` call through the shared pointer emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::pointers::lower_ptr_write_string(ctx, inst) +} diff --git a/src/builtins/pointers/buffer_free.rs b/src/builtins/pointers/buffer_free.rs new file mode 100644 index 0000000000..6be4bb05be --- /dev/null +++ b/src/builtins/pointers/buffer_free.rs @@ -0,0 +1,86 @@ +//! Purpose: +//! Home of the `buffer_free` builtin (elephc extension): 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` enforces the legacy checker-resident rules verbatim: the argument must +//! be a plain local variable (never `$this`, a by-ref parameter, a `global`, or a +//! `static`) of type `buffer`, because lowering nulls the local slot after the +//! free so use-after-free traps deterministically. +//! - `lower` is a thin wrapper over the shared `buffers::lower_buffer_free` emitter. +//! - `extension: true`: buffers have no PHP equivalent, so `--strict-php` hides this +//! builtin from user programs. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "buffer_free", + area: Pointers, + params: [buffer: Mixed], + returns: Void, + check: check, + lower: lower, + summary: "Frees a buffer and nulls the local variable that held it.", + extension: true, +} + +/// Validates that the argument is a freeable local `buffer` variable. +/// +/// Mirrors the legacy checker arm exactly: rejects `$this`, by-ref parameters, +/// `global` and `static` variables, and non-variable expressions, then requires +/// the argument type to be `buffer`. The registry's `check_arity` handles +/// arity enforcement (exactly 1 argument). +fn check(cx: &mut BuiltinCheckCtx) -> Result { + match &cx.args[0].kind { + ExprKind::Variable(name) => { + if cx.checker.current_class.is_some() && name == "this" { + return Err(CompileError::new(cx.span, "buffer_free() cannot free $this")); + } + if cx.checker.active_ref_params.contains(name) + || cx.checker.active_globals.contains(name) + || cx.checker.active_statics.contains(name) + { + return Err(CompileError::new( + cx.span, + "buffer_free() argument must be a local variable", + )); + } + } + _ => { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty, PhpType::Buffer(_)) { + return Err(CompileError::new( + cx.span, + "buffer_free() argument must be buffer", + )); + } + return Err(CompileError::new( + cx.span, + "buffer_free() argument must be a local variable", + )); + } + } + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty, PhpType::Buffer(_)) { + return Err(CompileError::new( + cx.span, + "buffer_free() argument must be buffer", + )); + } + Ok(PhpType::Void) +} + +/// Lowers a `buffer_free` call by dispatching to the shared buffers emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::buffers::lower_buffer_free(ctx, inst) +} diff --git a/src/builtins/pointers/buffer_len.rs b/src/builtins/pointers/buffer_len.rs new file mode 100644 index 0000000000..78dfbe9f32 --- /dev/null +++ b/src/builtins/pointers/buffer_len.rs @@ -0,0 +1,51 @@ +//! Purpose: +//! Home of the `buffer_len` builtin (elephc extension): its declaration, +//! type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the argument is a `buffer` and returns `PhpType::Int`, +//! preserving the legacy checker-resident arm's message verbatim. +//! - `lower` is a thin wrapper over the shared `buffers::lower_buffer_len` emitter. +//! - `extension: true`: buffers have no PHP equivalent, so `--strict-php` hides this +//! builtin from user programs. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "buffer_len", + area: Pointers, + params: [buffer: Mixed], + returns: Int, + check: check, + lower: lower, + summary: "Returns the logical element count of a buffer.", + extension: true, +} + +/// Validates that the argument is a `buffer` and returns `PhpType::Int`. +/// +/// The registry's `check_arity` handles arity enforcement (exactly 1 argument). +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty, PhpType::Buffer(_)) { + return Err(CompileError::new( + cx.span, + "buffer_len() argument must be buffer", + )); + } + Ok(PhpType::Int) +} + +/// Lowers a `buffer_len` call by dispatching to the shared buffers emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::buffers::lower_buffer_len(ctx, inst) +} diff --git a/src/builtins/pointers/mod.rs b/src/builtins/pointers/mod.rs index 93bc7896b3..ae5dd3599b 100644 --- a/src/builtins/pointers/mod.rs +++ b/src/builtins/pointers/mod.rs @@ -12,6 +12,11 @@ //! which `TypeSpec` cannot express statically. //! - Add `pub mod ;` here for every new pointer builtin home. +pub mod __elephc_ptr_is_null; +pub mod __elephc_ptr_read_string; +pub mod __elephc_ptr_write_string; +pub mod buffer_free; +pub mod buffer_len; pub mod ptr; pub mod ptr_get; pub mod ptr_is_null; diff --git a/src/builtins/pointers/ptr.rs b/src/builtins/pointers/ptr.rs index 1042b0746e..8b39894ec9 100644 --- a/src/builtins/pointers/ptr.rs +++ b/src/builtins/pointers/ptr.rs @@ -26,6 +26,7 @@ builtin! { check: check, lower: lower, summary: "Returns a raw pointer to the given variable.", + extension: true, } /// Validates that the argument is a variable and returns `PhpType::Pointer(None)`. diff --git a/src/builtins/pointers/ptr_get.rs b/src/builtins/pointers/ptr_get.rs index 09ea07714e..8d4ddffa6a 100644 --- a/src/builtins/pointers/ptr_get.rs +++ b/src/builtins/pointers/ptr_get.rs @@ -24,6 +24,7 @@ builtin! { check: check, lower: lower, summary: "Reads one machine word through a raw pointer and returns it as an integer.", + extension: true, } /// Validates that the argument is a pointer type and returns `PhpType::Int`. diff --git a/src/builtins/pointers/ptr_is_null.rs b/src/builtins/pointers/ptr_is_null.rs index 0bb8b9806e..fc56196ca8 100644 --- a/src/builtins/pointers/ptr_is_null.rs +++ b/src/builtins/pointers/ptr_is_null.rs @@ -24,12 +24,13 @@ builtin! { check: check, lower: lower, summary: "Returns true if the pointer is null.", + extension: true, } /// Validates that the argument is a pointer type and returns `PhpType::Bool`. /// /// The registry's `check_arity` handles arity enforcement (exactly 1 argument). -fn check(cx: &mut BuiltinCheckCtx) -> Result { +pub(crate) fn check(cx: &mut BuiltinCheckCtx) -> Result { let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; cx.checker.ensure_pointer_type(&ty, cx.span, "ptr_is_null()")?; Ok(PhpType::Bool) diff --git a/src/builtins/pointers/ptr_null.rs b/src/builtins/pointers/ptr_null.rs index ad96f2a589..ff1d84ccfd 100644 --- a/src/builtins/pointers/ptr_null.rs +++ b/src/builtins/pointers/ptr_null.rs @@ -25,6 +25,7 @@ builtin! { check: check, lower: lower, summary: "Returns a null raw pointer.", + extension: true, } /// Returns `PhpType::Pointer(None)` unconditionally (no arguments to validate). diff --git a/src/builtins/pointers/ptr_offset.rs b/src/builtins/pointers/ptr_offset.rs index 16ed75ea98..6c14db5f4c 100644 --- a/src/builtins/pointers/ptr_offset.rs +++ b/src/builtins/pointers/ptr_offset.rs @@ -25,6 +25,7 @@ builtin! { check: check, lower: lower, summary: "Returns a new pointer offset from the given pointer by the given byte count.", + extension: true, } /// Validates pointer and integer-compatible offset arguments and returns the pointer type. diff --git a/src/builtins/pointers/ptr_read16.rs b/src/builtins/pointers/ptr_read16.rs index 0ffb9916bc..6b0f18ec2d 100644 --- a/src/builtins/pointers/ptr_read16.rs +++ b/src/builtins/pointers/ptr_read16.rs @@ -24,6 +24,7 @@ builtin! { check: check, lower: lower, summary: "Reads one unsigned 16-bit word through a raw pointer and returns it as an integer.", + extension: true, } /// Validates that the argument is a pointer type and returns `PhpType::Int`. diff --git a/src/builtins/pointers/ptr_read32.rs b/src/builtins/pointers/ptr_read32.rs index 7d6c646bac..07f9fdd6c5 100644 --- a/src/builtins/pointers/ptr_read32.rs +++ b/src/builtins/pointers/ptr_read32.rs @@ -24,6 +24,7 @@ builtin! { check: check, lower: lower, summary: "Reads one unsigned 32-bit word through a raw pointer and returns it as an integer.", + extension: true, } /// Validates that the argument is a pointer type and returns `PhpType::Int`. diff --git a/src/builtins/pointers/ptr_read8.rs b/src/builtins/pointers/ptr_read8.rs index 15a3392f1a..423d5009a4 100644 --- a/src/builtins/pointers/ptr_read8.rs +++ b/src/builtins/pointers/ptr_read8.rs @@ -24,6 +24,7 @@ builtin! { check: check, lower: lower, summary: "Reads one unsigned byte through a raw pointer and returns it as an integer.", + extension: true, } /// Validates that the argument is a pointer type and returns `PhpType::Int`. diff --git a/src/builtins/pointers/ptr_read_string.rs b/src/builtins/pointers/ptr_read_string.rs index 97b4b0aeb4..7532b6b580 100644 --- a/src/builtins/pointers/ptr_read_string.rs +++ b/src/builtins/pointers/ptr_read_string.rs @@ -25,12 +25,13 @@ builtin! { check: check, lower: lower, summary: "Copies raw bytes from a pointer into a PHP string of the given length.", + extension: true, } /// Validates pointer and integer length arguments and returns `PhpType::Str`. /// /// The registry's `check_arity` handles arity enforcement (exactly 2 arguments). -fn check(cx: &mut BuiltinCheckCtx) -> Result { +pub(crate) fn check(cx: &mut BuiltinCheckCtx) -> Result { let ptr_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; cx.checker.ensure_pointer_type(&ptr_ty, cx.span, "ptr_read_string()")?; let len_ty = cx.checker.infer_type(&cx.args[1], cx.env)?; diff --git a/src/builtins/pointers/ptr_set.rs b/src/builtins/pointers/ptr_set.rs index 692a2d1be3..979ebed5cf 100644 --- a/src/builtins/pointers/ptr_set.rs +++ b/src/builtins/pointers/ptr_set.rs @@ -24,6 +24,7 @@ builtin! { check: check, lower: lower, summary: "Writes one machine word through a raw pointer.", + extension: true, } /// Validates pointer and word-compatible value arguments and returns `PhpType::Void`. diff --git a/src/builtins/pointers/ptr_sizeof.rs b/src/builtins/pointers/ptr_sizeof.rs index a11a4e74c1..e8c9a6dc52 100644 --- a/src/builtins/pointers/ptr_sizeof.rs +++ b/src/builtins/pointers/ptr_sizeof.rs @@ -26,6 +26,7 @@ builtin! { check: check, lower: lower, summary: "Returns the byte size of the named pointer target type.", + extension: true, } /// Validates that the argument is a known string literal type name and returns `PhpType::Int`. diff --git a/src/builtins/pointers/ptr_write16.rs b/src/builtins/pointers/ptr_write16.rs index e8ef09f4a2..a45ff68deb 100644 --- a/src/builtins/pointers/ptr_write16.rs +++ b/src/builtins/pointers/ptr_write16.rs @@ -24,6 +24,7 @@ builtin! { check: check, lower: lower, summary: "Writes one 16-bit word through a raw pointer.", + extension: true, } /// Validates pointer and integer value arguments and returns `PhpType::Void`. diff --git a/src/builtins/pointers/ptr_write32.rs b/src/builtins/pointers/ptr_write32.rs index f8db123f21..cf349e8e38 100644 --- a/src/builtins/pointers/ptr_write32.rs +++ b/src/builtins/pointers/ptr_write32.rs @@ -24,6 +24,7 @@ builtin! { check: check, lower: lower, summary: "Writes one 32-bit word through a raw pointer.", + extension: true, } /// Validates pointer and integer value arguments and returns `PhpType::Void`. diff --git a/src/builtins/pointers/ptr_write8.rs b/src/builtins/pointers/ptr_write8.rs index 6168b770a3..081d213d16 100644 --- a/src/builtins/pointers/ptr_write8.rs +++ b/src/builtins/pointers/ptr_write8.rs @@ -24,6 +24,7 @@ builtin! { check: check, lower: lower, summary: "Writes one byte through a raw pointer.", + extension: true, } /// Validates pointer and integer value arguments and returns `PhpType::Void`. diff --git a/src/builtins/pointers/ptr_write_string.rs b/src/builtins/pointers/ptr_write_string.rs index 42d357f394..42568b92ab 100644 --- a/src/builtins/pointers/ptr_write_string.rs +++ b/src/builtins/pointers/ptr_write_string.rs @@ -25,13 +25,14 @@ builtin! { check: check, lower: lower, summary: "Copies PHP string bytes into raw memory at the given pointer.", + extension: true, } /// Validates pointer and string arguments and returns `PhpType::Int`. /// /// The registry's `check_arity` handles arity enforcement (exactly 2 arguments). /// Returns the number of bytes written as an integer. -fn check(cx: &mut BuiltinCheckCtx) -> Result { +pub(crate) fn check(cx: &mut BuiltinCheckCtx) -> Result { let ptr_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; cx.checker.ensure_pointer_type(&ptr_ty, cx.span, "ptr_write_string()")?; let str_ty = cx.checker.infer_type(&cx.args[1], cx.env)?; diff --git a/src/builtins/pointers/zval_free.rs b/src/builtins/pointers/zval_free.rs index 42bf6c691b..cd1185eeb0 100644 --- a/src/builtins/pointers/zval_free.rs +++ b/src/builtins/pointers/zval_free.rs @@ -24,6 +24,7 @@ builtin! { check: check, lower: lower, summary: "Frees a PHP zval pointer allocated by `zval_pack`.", + extension: true, } /// Validates the zval pointer argument and returns `PhpType::Void`. diff --git a/src/builtins/pointers/zval_pack.rs b/src/builtins/pointers/zval_pack.rs index 04de2c5e01..66d5939138 100644 --- a/src/builtins/pointers/zval_pack.rs +++ b/src/builtins/pointers/zval_pack.rs @@ -24,6 +24,7 @@ builtin! { check: check, lower: lower, summary: "Packs an elephc runtime value into a heap-allocated PHP zval pointer.", + extension: true, } /// Accepts any value and returns an untyped raw pointer to the allocated zval. diff --git a/src/builtins/pointers/zval_type.rs b/src/builtins/pointers/zval_type.rs index 90f58a91a4..033f1f0c3f 100644 --- a/src/builtins/pointers/zval_type.rs +++ b/src/builtins/pointers/zval_type.rs @@ -24,6 +24,7 @@ builtin! { check: check, lower: lower, summary: "Returns the PHP zval type byte for a zval pointer.", + extension: true, } /// Validates the zval pointer argument and returns `PhpType::Int`. diff --git a/src/builtins/pointers/zval_unpack.rs b/src/builtins/pointers/zval_unpack.rs index 4cdfe37746..9c2ab2722b 100644 --- a/src/builtins/pointers/zval_unpack.rs +++ b/src/builtins/pointers/zval_unpack.rs @@ -24,6 +24,7 @@ builtin! { check: check, lower: lower, summary: "Unpacks a PHP zval pointer into an owned elephc Mixed value.", + extension: true, } /// Validates the zval pointer argument and returns `PhpType::Mixed`. diff --git a/src/builtins/registry.rs b/src/builtins/registry.rs index 98920d379e..3c223466f1 100644 --- a/src/builtins/registry.rs +++ b/src/builtins/registry.rs @@ -255,6 +255,16 @@ fn refine_first_class_callable_sig(name: &str, sig: &mut FunctionSig) { } sig.return_type = PhpType::Void; } + // Preserves the legacy typed first-class signature: the registry param is + // Mixed by generality, but the wrapper must demand a buffer so first-class + // use keeps rejecting non-buffer arguments at check time instead of + // failing later in codegen. + "buffer_len" => { + if let Some((_, buffer_ty)) = sig.params.get_mut(0) { + *buffer_ty = PhpType::Buffer(Box::new(PhpType::Int)); + } + sig.return_type = PhpType::Int; + } _ => {} } } diff --git a/src/builtins/spec.rs b/src/builtins/spec.rs index 725a5cf82d..f7e79a3e65 100644 --- a/src/builtins/spec.rs +++ b/src/builtins/spec.rs @@ -235,6 +235,13 @@ pub struct BuiltinSpec { pub php_manual: Option<&'static str>, /// A deprecation message, or `None` if the builtin is not deprecated. pub deprecation: Option<&'static str>, + /// When `true`, the builtin is an elephc extension with no PHP equivalent + /// (`ptr_*`, `zval_*`, `buffer_*`, `class_attribute_*`, …). `--strict-php` + /// hides extension builtins from user programs: they stop resolving as + /// builtins, and user code may declare functions with these names, exactly + /// as under the PHP interpreter. The set is pinned by + /// `parity_tests::extension_builtin_set_is_pinned`. + pub extension: bool, /// When `true`, the builtin is not PHP-visible and is not emitted in catalogs /// or documentation; it is only used internally by the compiler. pub internal: bool, @@ -251,6 +258,7 @@ mod macro_tests { builtin! { name: "__macro_probe", area: Internal, params: [x: Int], returns: Int, lower: lower, summary: "probe", internal: true } builtin! { name: "__macro_owned_probe", area: Internal, params: [], returns: Mixed, returns_fresh_storage: true, lower: lower, summary: "owned probe", internal: true } builtin! { name: "__macro_independent_probe", area: Internal, params: [value: Str], returns: Str, returns_independent_storage: true, lower: lower, summary: "independent probe", internal: true } + builtin! { name: "__macro_ext_probe", area: Internal, params: [], returns: Null, lower: lower, summary: "extension probe", extension: true, internal: true } /// Verifies macro registration and the fresh/independent storage metadata defaults. #[test] @@ -272,6 +280,22 @@ mod macro_tests { assert!(owned_spec.returns_fresh_storage); assert!(independent_spec.returns_independent_storage); } + + /// Verifies the `extension` flag defaults to false and is set by the macro arm, + /// so `--strict-php` classification is opt-in per builtin declaration. + #[test] + fn macro_registers_extension_flag() { + let default_spec = inventory::iter:: + .into_iter() + .find(|s| s.name == "__macro_probe") + .expect("macro probe must be registered"); + let ext_spec = inventory::iter:: + .into_iter() + .find(|s| s.name == "__macro_ext_probe") + .expect("extension probe must be registered"); + assert!(!default_spec.extension); + assert!(ext_spec.extension); + } } #[cfg(test)] @@ -289,7 +313,7 @@ mod tests { returns_independent_storage: false, by_ref_return: false, check: None, lazy_check: false, lower: noop_lower, summary: "len", examples: &[], php_manual: None, - deprecation: None, internal: false, + deprecation: None, extension: false, internal: false, }; assert_eq!(S.name, "strlen"); assert_eq!(S.params.len(), 1); diff --git a/src/builtins/system/class_attribute_args.rs b/src/builtins/system/class_attribute_args.rs index f0c997e906..adba2b29ee 100644 --- a/src/builtins/system/class_attribute_args.rs +++ b/src/builtins/system/class_attribute_args.rs @@ -30,6 +30,7 @@ builtin! { check: check, lower: lower, summary: "Returns the constructor arguments of a named attribute applied to a class.", + extension: true, } /// Validates both arguments are string literals, resolves the class and attribute, diff --git a/src/builtins/system/class_attribute_names.rs b/src/builtins/system/class_attribute_names.rs index bf96d8a690..78d42f2b0b 100644 --- a/src/builtins/system/class_attribute_names.rs +++ b/src/builtins/system/class_attribute_names.rs @@ -29,6 +29,7 @@ builtin! { check: check, lower: lower, summary: "Returns the list of attribute names applied to a class.", + extension: true, } /// Validates that the argument is a string literal class name, resolves the class, diff --git a/src/builtins/system/class_get_attributes.rs b/src/builtins/system/class_get_attributes.rs index d7408b1fbb..9989f2979c 100644 --- a/src/builtins/system/class_get_attributes.rs +++ b/src/builtins/system/class_get_attributes.rs @@ -30,6 +30,7 @@ builtin! { check: check, lower: lower, summary: "Returns an array of ReflectionAttribute objects for all attributes of a class.", + extension: true, } /// Validates that the argument is a string literal class name, resolves the class, diff --git a/src/cli.rs b/src/cli.rs index 0bb0126afc..19521acaae 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -15,7 +15,7 @@ pub(crate) use crate::codegen::Emit; use crate::codegen::platform::Target; /// Usage string printed to stderr when command-line arguments are invalid or missing. -pub(crate) const USAGE: &str = "Usage: elephc [--target TARGET] [--php-version 8.2|8.3|8.4|8.5] [--heap-size=BYTES] [--gc-stats] [--heap-debug] [--emit-ir] [--emit-asm] [--emit KIND] [--check] [--null-repr=sentinel|tagged] [--regalloc=linear|stack] [--ir-opt=on|off] [--timings] [--source-map] [--debug-info] [--define SYMBOL] [--link LIB|-lLIB] [--link-path DIR|-LDIR] [--framework NAME] [--web] [--with-CRATE] "; +pub(crate) const USAGE: &str = "Usage: elephc [--target TARGET] [--php-version 8.2|8.3|8.4|8.5] [--heap-size=BYTES] [--gc-stats] [--heap-debug] [--emit-ir] [--emit-asm] [--emit KIND] [--check] [--strict-php] [--null-repr=sentinel|tagged] [--regalloc=linear|stack] [--ir-opt=on|off] [--timings] [--source-map] [--debug-info] [--define SYMBOL] [--link LIB|-lLIB] [--link-path DIR|-LDIR] [--framework NAME] [--web] [--with-CRATE] "; /// Configuration derived from command-line arguments, passed to the compile pipeline. /// Controls heap allocation size, debug output, code generation options, and linking behavior. @@ -42,6 +42,9 @@ pub(crate) struct CliConfig { pub(crate) extra_link_paths: Vec, pub(crate) extra_frameworks: Vec, pub(crate) defines: HashSet, + /// Accept only PHP-compatible constructs: elephc extensions (`ptr`, `buffer`, + /// `packed class`, `extern`, `ifdef`, extension builtins) become compile errors. + pub(crate) strict_php: bool, pub(crate) web: bool, /// Bridge crates the user force-enabled with `--with-` (short flag /// names such as `"pdo"`). Each one force-links the matching staticlib and, @@ -75,6 +78,7 @@ pub(crate) fn parse_args(args: &[String]) -> CliConfig { let mut extra_link_paths: Vec = Vec::new(); let mut extra_frameworks: Vec = Vec::new(); let mut defines: HashSet = HashSet::new(); + let mut strict_php = false; let mut web = false; let mut with_crates: HashSet = HashSet::new(); let mut null_repr = match std::env::var("ELEPHC_NULL_REPR").as_deref() { @@ -174,6 +178,8 @@ pub(crate) fn parse_args(args: &[String]) -> CliConfig { i, "Missing framework name after --framework", )); + } else if arg == "--strict-php" { + strict_php = true; } else if arg == "--web" { web = true; } else if let Some(name) = arg.strip_prefix("--with-") { @@ -223,6 +229,9 @@ pub(crate) fn parse_args(args: &[String]) -> CliConfig { if web && emit_ir { fail("--web cannot be combined with --emit-ir"); } + if let Err(message) = validate_strict_php_defines(strict_php, &defines) { + fail(message); + } CliConfig { filename, @@ -245,6 +254,7 @@ pub(crate) fn parse_args(args: &[String]) -> CliConfig { extra_link_paths, extra_frameworks, defines, + strict_php, web, with_crates, } @@ -272,6 +282,21 @@ fn parse_php_version(value: &str) -> crate::web_prelude::PhpVersion { }) } +/// Validates that `--strict-php` is not combined with `--define` symbols. +/// +/// Defines only feed `ifdef` conditional compilation, which strict mode rejects +/// outright, so the combination is always a configuration mistake. Kept pure +/// (no IO/exit) so the rule can be unit-tested. +fn validate_strict_php_defines( + strict_php: bool, + defines: &HashSet, +) -> Result<(), &'static str> { + if strict_php && !defines.is_empty() { + return Err("--strict-php cannot be combined with --define: ifdef conditional compilation is an elephc extension"); + } + Ok(()) +} + /// Parse the required emit-kind argument at the given index, or fail if missing. fn parse_required_emit(args: &[String], index: usize) -> Emit { if index < args.len() { @@ -514,4 +539,42 @@ mod tests { let config = parse_args(&args); assert!(config.with_crates.is_empty()); } + + /// Verifies `--strict-php` sets the strict-PHP flag on the parsed config. + #[test] + fn strict_php_flag_sets_strict() { + let args = vec!["elephc".into(), "--strict-php".into(), "app.php".into()]; + let config = parse_args(&args); + assert!(config.strict_php); + } + + /// Verifies the absence of `--strict-php` leaves strict mode off. + #[test] + fn no_strict_php_flag_defaults_off() { + let args = vec!["elephc".into(), "app.php".into()]; + let config = parse_args(&args); + assert!(!config.strict_php); + } + + /// Verifies `--strict-php` combined with `--define` is rejected: defines only + /// feed `ifdef` conditional compilation, which strict mode rejects outright, + /// so accepting the combination would silently hide a configuration mistake. + #[test] + fn strict_php_with_define_conflict_is_rejected() { + let mut defines = HashSet::new(); + defines.insert("FEATURE".to_string()); + assert!(validate_strict_php_defines(true, &defines).is_err()); + } + + /// Verifies `--strict-php` without defines and `--define` without strict mode + /// both pass the conflict validation. + #[test] + fn strict_php_defines_validation_accepts_non_conflicting() { + let empty = HashSet::new(); + let mut defines = HashSet::new(); + defines.insert("FEATURE".to_string()); + assert!(validate_strict_php_defines(true, &empty).is_ok()); + assert!(validate_strict_php_defines(false, &defines).is_ok()); + assert!(validate_strict_php_defines(false, &empty).is_ok()); + } } diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index 1a04552d64..f189924150 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -28,7 +28,7 @@ use crate::codegen::{CodegenIrError, Result}; pub(crate) mod attributes; pub(crate) mod arrays; -mod buffers; +pub(crate) mod buffers; pub(crate) mod class_relations; pub(crate) mod ctype; pub(crate) mod debug; @@ -64,8 +64,6 @@ pub(super) fn lower_builtin_call(ctx: &mut FunctionContext<'_>, inst: &Instructi match key.as_str() { "closure_bind" => lower_closure_bind(ctx, inst), "eval" => eval::lower_eval(ctx, inst), - "buffer_len" => buffers::lower_buffer_len(ctx, inst), - "buffer_free" => buffers::lower_buffer_free(ctx, inst), "strval" => lower_strval(ctx, inst), "method_exists" | "property_exists" => lower_member_exists(ctx, inst, key.as_str()), "is_integer" | "is_long" => { diff --git a/src/codegen/lower_inst/builtins/buffers.rs b/src/codegen/lower_inst/builtins/buffers.rs index 87eb7f3e12..ea744b88de 100644 --- a/src/codegen/lower_inst/builtins/buffers.rs +++ b/src/codegen/lower_inst/builtins/buffers.rs @@ -3,7 +3,8 @@ //! Keeps builtin-name dispatch separate from direct buffer opcodes like `BufferNew`. //! //! Called from: -//! - `crate::codegen::lower_inst::builtins::lower_builtin_call()`. +//! - The `buffer_len`/`buffer_free` registry lowering hooks +//! (`src/builtins/pointers/buffer_len.rs`, `src/builtins/pointers/buffer_free.rs`). //! //! Key details: //! - Buffer helpers use shared runtime symbols so fatal behavior and buffer @@ -16,11 +17,11 @@ use super::super::buffers; use super::super::super::context::FunctionContext; /// Lowers `buffer_len()` through the direct buffer opcode helper. -pub(super) fn lower_buffer_len(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { +pub(crate) fn lower_buffer_len(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { buffers::lower_buffer_len(ctx, inst) } /// Lowers `buffer_free()` through the direct buffer opcode helper. -pub(super) fn lower_buffer_free(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { +pub(crate) fn lower_buffer_free(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { buffers::lower_buffer_free(ctx, inst) } diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 308c6639fa..e807644f7c 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -6370,12 +6370,30 @@ fn ensure_eval_context(ctx: &mut FunctionContext<'_>) -> Result<()> { register_eval_declared_symbols(ctx, offset); register_eval_native_functions(ctx, offset)?; register_eval_native_method_signatures(ctx, offset); + mark_eval_strict_php(ctx); ctx.emitter.label(&ready); abi::load_at_offset(ctx.emitter, result_reg, offset); abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_CONTEXT_HANDLE_OFFSET); Ok(()) } +/// Marks the eval bridge as strict-PHP when this compilation runs with +/// `--strict-php`, so runtime eval hides extension builtins exactly like the +/// AOT surface does. Emits nothing in normal compilations: non-strict binaries +/// never reference the setter symbol, and the bridge's flag defaults to off. +fn mark_eval_strict_php(ctx: &mut FunctionContext<'_>) { + if !crate::strict_php::is_enabled() { + return; + } + let arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); + abi::emit_load_int_immediate(ctx.emitter, arg_reg, 1); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_set_strict_php"); + abi::emit_call_label(ctx.emitter, &symbol); +} + /// Returns the hidden frame slot that owns this function's persistent eval context. fn eval_context_slot(ctx: &FunctionContext<'_>) -> Result { ctx.function diff --git a/src/eval_aot.rs b/src/eval_aot.rs index 7dab17efe2..9dde8f9b65 100644 --- a/src/eval_aot.rs +++ b/src/eval_aot.rs @@ -1268,7 +1268,15 @@ fn stmt_fallback_reason(stmt: &Stmt) -> Option { StmtKind::Echo(expr) | StmtKind::ExprStmt(expr) | StmtKind::Return(Some(expr)) => { expr_fallback_reason(expr) } - StmtKind::Assign { value, .. } | StmtKind::TypedAssign { value, .. } => { + StmtKind::Assign { value, .. } => expr_fallback_reason(value), + // Typed local declarations are an elephc extension: under `--strict-php` + // the fragment must reach the runtime bridge, whose parser rejects the + // syntax like the PHP interpreter would (runtime parse error), instead + // of being AOT-compiled and silently executing non-PHP code. + StmtKind::TypedAssign { value, .. } => { + if crate::strict_php::is_enabled() { + return Some(EvalAotFallbackReason::UnsupportedConstruct); + } expr_fallback_reason(value) } StmtKind::If { @@ -1330,11 +1338,19 @@ fn stmt_fallback_reason(stmt: &Stmt) -> Option { then_body, else_body, .. - } => then_body.iter().find_map(stmt_fallback_reason).or_else(|| { - else_body - .as_deref() - .and_then(|body| body.iter().find_map(stmt_fallback_reason)) - }), + } => { + // `ifdef` is an elephc extension: under `--strict-php` the fragment + // must reach the runtime bridge, whose parser rejects the syntax + // like the PHP interpreter would, instead of being AOT-compiled. + if crate::strict_php::is_enabled() { + return Some(EvalAotFallbackReason::UnsupportedConstruct); + } + then_body.iter().find_map(stmt_fallback_reason).or_else(|| { + else_body + .as_deref() + .and_then(|body| body.iter().find_map(stmt_fallback_reason)) + }) + } } } diff --git a/src/image_prelude.rs b/src/image_prelude.rs index e77c89ed45..cfd7c86a00 100644 --- a/src/image_prelude.rs +++ b/src/image_prelude.rs @@ -33,7 +33,7 @@ mod detect; /// The elephc-PHP image prelude: `elephc_image` externs, `IMAGETYPE_*` /// constants, the `GdImage` class, and the procedural image functions. -const IMAGE_PRELUDE_SRC: &str = r#"= 2. $_len = strlen($iptcdata); $_buf = elephc_img_in_ptr($_len); - if (ptr_is_null($_buf)) { + if (__elephc_ptr_is_null($_buf)) { return false; } - ptr_write_string($_buf, $iptcdata); + __elephc_ptr_write_string($_buf, $iptcdata); $_outlen = elephc_iptc_embed($jpeg_file_name, $_len); if ($_outlen < 0) { return false; } - $_bytes = ptr_read_string(elephc_img_out_ptr(), $_outlen); + $_bytes = __elephc_ptr_read_string(elephc_img_out_ptr(), $_outlen); if ($spool >= 2) { echo $_bytes; } @@ -2309,10 +2309,10 @@ class Imagick implements Iterator, Countable { throw new ImagickException("Imagick::readImageBlob(): empty blob"); } $_buf = elephc_img_stage_ptr($_len); - if (ptr_is_null($_buf)) { + if (__elephc_ptr_is_null($_buf)) { throw new ImagickException("Imagick::readImageBlob(): allocation failed"); } - ptr_write_string($_buf, $image); + __elephc_ptr_write_string($_buf, $image); if (elephc_imagick_read_blob($this->wand, $_len) !== 0) { throw new ImagickException("Imagick::readImageBlob(): unrecognized image data"); } @@ -2357,7 +2357,7 @@ class Imagick implements Iterator, Countable { if ($_len < 0) { throw new ImagickException("Imagick::getImageBlob(): no image or encode failed"); } - $_bytes = ptr_read_string(elephc_img_encoded_ptr(), $_len); + $_bytes = __elephc_ptr_read_string(elephc_img_encoded_ptr(), $_len); elephc_img_encoded_clear(); return $_bytes; } @@ -4531,10 +4531,10 @@ class Gmagick { throw new GmagickException("Gmagick::readimageblob(): empty blob"); } $_buf = elephc_img_stage_ptr($_len); - if (ptr_is_null($_buf)) { + if (__elephc_ptr_is_null($_buf)) { throw new GmagickException("Gmagick::readimageblob(): allocation failed"); } - ptr_write_string($_buf, $image); + __elephc_ptr_write_string($_buf, $image); if (elephc_imagick_read_blob($this->wand, $_len) !== 0) { throw new GmagickException("Gmagick::readimageblob(): unrecognized image data"); } @@ -4570,7 +4570,7 @@ class Gmagick { if ($_len < 0) { throw new GmagickException("Gmagick::getimageblob(): no image or encode failed"); } - $_bytes = ptr_read_string(elephc_img_encoded_ptr(), $_len); + $_bytes = __elephc_ptr_read_string(elephc_img_encoded_ptr(), $_len); elephc_img_encoded_clear(); return $_bytes; } diff --git a/src/ir_lower/tests/corpus.rs b/src/ir_lower/tests/corpus.rs index 826f00295c..16a7afdbc6 100644 --- a/src/ir_lower/tests/corpus.rs +++ b/src/ir_lower/tests/corpus.rs @@ -11,6 +11,11 @@ use std::path::{Path, PathBuf}; /// Verifies every checked example program lowers to validated printable EIR. +/// +/// The `strict-php` example is lowered with strict-PHP mode enabled, matching +/// its documented `elephc --strict-php` invocation: it deliberately declares a +/// user function named after an extension builtin, which only PHP-compatible +/// (strict) resolution accepts. #[test] fn lowers_examples_corpus() { let root = Path::new(env!("CARGO_MANIFEST_DIR")); @@ -19,6 +24,14 @@ fn lowers_examples_corpus() { assert!(!fixtures.is_empty(), "expected example PHP fixtures"); for fixture in fixtures { + let strict = fixture + .parent() + .and_then(|dir| dir.file_name()) + .is_some_and(|name| name == "strict-php"); + // RAII guard: if lowering a strict fixture panics, the guard still + // restores the state during unwinding, so no later fixture can + // accidentally run with strict mode inherited. + let _guard = strict.then(crate::strict_php::scoped_enable); let module = super::lower_file(&fixture); assert!( !module.functions.is_empty(), diff --git a/src/lib.rs b/src/lib.rs index 1963a36daa..926841146e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,6 +55,8 @@ pub mod pdo_prelude; pub mod resolver; /// Source span tracking. pub mod span; +/// `--strict-php` mode state and PHP-compatibility audit pass. +pub mod strict_php; mod string_bytes; /// Canonical HTTP-request superglobal set and shared type helper. pub mod superglobals; diff --git a/src/list_id_prelude.rs b/src/list_id_prelude.rs index 2dc271a569..bb4c68d7bb 100644 --- a/src/list_id_prelude.rs +++ b/src/list_id_prelude.rs @@ -30,7 +30,7 @@ mod table; /// The `__elephc_list_identifiers` source, with `__ELEPHC_TZ_GROUPS_TABLE__` as a /// placeholder spliced with the baked table at injection time. `replace` is used /// rather than `format!` so the PHP body's `{`/`}` need no escaping. -const LIST_ID_PRELUDE_TEMPLATE: &str = r#" = const { Cell::new(false) }; +} + +/// Enables or disables strict-PHP mode for the current thread's compilation. +pub fn set_enabled(enabled: bool) { + STRICT_PHP.with(|cell| cell.set(enabled)); +} + +/// Returns whether strict-PHP mode is active for the current thread's compilation. +pub fn is_enabled() -> bool { + STRICT_PHP.with(|cell| cell.get()) +} + +/// RAII guard restoring the previous strict-mode state on drop. +/// +/// Test fixtures that enable strict mode must hold one of these instead of +/// calling `set_enabled` in pairs: if an assertion panics mid-fixture, the +/// guard's `Drop` still runs during unwinding, so later fixtures on the same +/// thread (e.g. the examples corpus loop) can never inherit stale strict state. +/// +/// Constructed only by lib unit tests and integration tests; the compiler +/// binary sets the flag once from the CLI and never scopes it, hence the +/// dead-code allowance for the bin target. +#[allow(dead_code)] +pub struct StrictModeGuard { + previous: bool, +} + +impl Drop for StrictModeGuard { + /// Restores the strict-mode state captured when the guard was created. + fn drop(&mut self) { + set_enabled(self.previous); + } +} + +/// Enables strict mode and returns a guard that restores the previous state on drop. +/// +/// Used by lib unit tests and integration tests only (see `StrictModeGuard`). +#[allow(dead_code)] +pub fn scoped_enable() -> StrictModeGuard { + let previous = is_enabled(); + set_enabled(true); + StrictModeGuard { previous } +} + +/// Audits one user source file's parsed statements when strict mode is active. +/// +/// No-op (always `Ok`) when strict mode is off. Violations are bundled into a +/// single `CompileError` (primary + related) attributed to `file`, so callers +/// that parse user files mid-pipeline — the include resolver and the autoloader +/// — can propagate every violation through their existing single-error paths. +/// Every call site runs it on the parsed AST right after magic-constant +/// substitution (a PHP-compatible rewrite) and before any pass synthesizes +/// compiler-internal names or nodes into it. +pub fn check_file( + program: &crate::parser::ast::Program, + file: &str, +) -> Result<(), crate::errors::CompileError> { + if !is_enabled() { + return Ok(()); + } + let violations = check(program); + if violations.is_empty() { + return Ok(()); + } + Err(crate::errors::CompileError::from_many(violations).with_file(file.to_string())) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Verifies strict mode defaults to off so ordinary compiles are unaffected. + #[test] + fn strict_mode_defaults_off() { + assert!(!is_enabled()); + } + + /// Verifies enabling and disabling strict mode round-trips on the same thread. + #[test] + fn strict_mode_set_and_clear_roundtrip() { + set_enabled(true); + assert!(is_enabled()); + set_enabled(false); + assert!(!is_enabled()); + } + + /// Verifies the RAII guard restores the previous state on drop, including + /// when dropped during a panic unwind, so panicking fixtures cannot leak + /// strict state to later fixtures on the same thread. + #[test] + fn scoped_enable_restores_on_drop_and_panic() { + { + let _guard = scoped_enable(); + assert!(is_enabled()); + } + assert!(!is_enabled()); + + let unwind = std::panic::catch_unwind(|| { + let _guard = scoped_enable(); + panic!("fixture failure"); + }); + assert!(unwind.is_err()); + assert!(!is_enabled(), "guard must restore state during unwinding"); + } + + /// Verifies strict mode is thread-local: enabling it on another thread must + /// not leak into this thread's compilation state. + #[test] + fn strict_mode_is_thread_local() { + let handle = std::thread::spawn(|| { + set_enabled(true); + is_enabled() + }); + assert!(handle.join().expect("thread must not panic")); + assert!(!is_enabled()); + } +} diff --git a/src/strict_php/audit.rs b/src/strict_php/audit.rs new file mode 100644 index 0000000000..65e3806c8b --- /dev/null +++ b/src/strict_php/audit.rs @@ -0,0 +1,748 @@ +//! Purpose: +//! The `--strict-php` AST audit pass: walks a parsed user program and reports a +//! `CompileError` for every elephc-only construct (`ifdef`, `packed class`, +//! `extern`, `ptr_cast`, `buffer_new`, typed local declarations, +//! `ptr`/`buffer` type annotations, and compiler-reserved `__elephc_*` names) +//! in every expression position, including PHP attribute arguments. +//! +//! Called from: +//! - `crate::pipeline::compile()` — on the main file right after parsing, and on +//! the resolved program (which includes `include`/`require`d user files) before +//! any compiler prelude is injected, so injected compiler code is never audited. +//! - Integration tests through `crate::strict_php::check`. +//! +//! Key details: +//! - Every `match` on `StmtKind`, `ExprKind`, and `TypeExpr` is exhaustive with +//! no wildcard arm: adding a new AST variant fails compilation here until the +//! author decides whether the construct is PHP or an elephc extension. +//! - All violations are collected in one pass (not first-error-only) so a file +//! can be fixed in a single round. +//! - `TypeExpr` carries no span, so type violations reuse the enclosing +//! statement's or expression's span. + +use crate::errors::CompileError; +use crate::names::Name; +use crate::parser::ast::{ + AttributeGroup, CallableTarget, CatchClause, ClassConst, ClassMethod, ClassProperty, + EnumCaseDecl, Expr, ExprKind, InstanceOfTarget, Program, Stmt, StmtKind, TypeExpr, +}; +use crate::span::Span; + +/// Audits a parsed user program and returns every strict-PHP violation found. +/// +/// An empty result means the program uses only PHP-compatible constructs at the +/// syntax level (builtin availability is enforced separately by the catalog and +/// checker). The caller decides how to report the collected errors. +pub fn check(program: &Program) -> Vec { + let mut errors = Vec::new(); + audit_stmts(program, &mut errors); + errors +} + +/// Pushes a violation for an elephc-only construct with the standard suffix. +fn reject(errors: &mut Vec, span: Span, lead: &str) { + errors.push(CompileError::new( + span, + &format!("{lead}; rejected by --strict-php"), + )); +} + +/// Pushes a violation when `name`'s bare (last) segment uses the +/// compiler-reserved `__elephc_` prefix. +/// +/// Applied to free-function declarations and call targets only: the compiler +/// reserves the prefix exclusively for internal builtins and synthesized +/// functions, which live in the function namespace. Class, method, and property +/// names are separate PHP symbol spaces the compiler never synthesizes +/// `__elephc_*` names into, so they are deliberately not checked. +fn reject_reserved_name(errors: &mut Vec, span: Span, name: &Name) { + let bare = name.parts.last().map(String::as_str).unwrap_or_default(); + if bare.to_ascii_lowercase().starts_with("__elephc_") { + reject( + errors, + span, + &format!("'{bare}' is reserved for the compiler ('__elephc_' prefix)"), + ); + } +} + +/// Audits a statement list in order. +fn audit_stmts(stmts: &[Stmt], errors: &mut Vec) { + for stmt in stmts { + audit_stmt(stmt, errors); + } +} + +/// Audits PHP attribute groups: attribute arguments are ordinary expressions +/// (`#[Foo(buffer_new(4))]` is a real call site once the attribute is +/// reflected), so they get the same expression audit as any other code. +fn audit_attribute_groups(groups: &[AttributeGroup], errors: &mut Vec) { + for group in groups { + for attribute in &group.attributes { + audit_exprs(&attribute.args, errors); + } + } +} + +/// Audits one statement: rejects extension statement forms and recurses into +/// every nested expression, type annotation, statement body, and the +/// statement's own attribute arguments. +fn audit_stmt(stmt: &Stmt, errors: &mut Vec) { + let span = stmt.span; + audit_attribute_groups(&stmt.attributes, errors); + match &stmt.kind { + StmtKind::Echo(expr) => audit_expr(expr, errors), + StmtKind::Assign { name: _, value } => audit_expr(value, errors), + StmtKind::RefAssign { target: _, source } => audit_expr(source, errors), + StmtKind::If { + condition, + then_body, + elseif_clauses, + else_body, + } => { + audit_expr(condition, errors); + audit_stmts(then_body, errors); + for (clause_condition, clause_body) in elseif_clauses { + audit_expr(clause_condition, errors); + audit_stmts(clause_body, errors); + } + if let Some(body) = else_body { + audit_stmts(body, errors); + } + } + StmtKind::IfDef { + symbol: _, + then_body, + else_body, + } => { + reject( + errors, + span, + "`ifdef` conditional compilation is an elephc extension and is not valid PHP", + ); + audit_stmts(then_body, errors); + if let Some(body) = else_body { + audit_stmts(body, errors); + } + } + StmtKind::While { condition, body } => { + audit_expr(condition, errors); + audit_stmts(body, errors); + } + StmtKind::DoWhile { body, condition } => { + audit_stmts(body, errors); + audit_expr(condition, errors); + } + StmtKind::For { + init, + condition, + update, + body, + } => { + if let Some(init) = init { + audit_stmt(init, errors); + } + if let Some(condition) = condition { + audit_expr(condition, errors); + } + if let Some(update) = update { + audit_stmt(update, errors); + } + audit_stmts(body, errors); + } + StmtKind::ArrayAssign { + array: _, + index, + value, + } => { + audit_expr(index, errors); + audit_expr(value, errors); + } + StmtKind::NestedArrayAssign { target, value } => { + audit_expr(target, errors); + audit_expr(value, errors); + } + StmtKind::ArrayPush { array: _, value } => audit_expr(value, errors), + StmtKind::TypedAssign { + type_expr, + name: _, + value, + } => { + reject( + errors, + span, + "typed local variable declarations are an elephc extension and are not valid PHP", + ); + audit_type(type_expr, span, errors); + audit_expr(value, errors); + } + StmtKind::Foreach { + array, + key_var: _, + value_var: _, + value_by_ref: _, + body, + } => { + audit_expr(array, errors); + audit_stmts(body, errors); + } + StmtKind::Switch { + subject, + cases, + default, + } => { + audit_expr(subject, errors); + for (case_values, case_body) in cases { + for value in case_values { + audit_expr(value, errors); + } + audit_stmts(case_body, errors); + } + if let Some(body) = default { + audit_stmts(body, errors); + } + } + StmtKind::Include { + path, + once: _, + required: _, + } => audit_expr(path, errors), + StmtKind::IncludeOnceMark { label: _ } => {} + StmtKind::IncludeOnceGuard { label: _, body } => audit_stmts(body, errors), + StmtKind::Throw(expr) => audit_expr(expr, errors), + StmtKind::Synthetic(body) => audit_stmts(body, errors), + StmtKind::Try { + try_body, + catches, + finally_body, + } => { + audit_stmts(try_body, errors); + for CatchClause { + exception_types: _, + variable: _, + body, + } in catches + { + audit_stmts(body, errors); + } + if let Some(body) = finally_body { + audit_stmts(body, errors); + } + } + StmtKind::Break(_) | StmtKind::Continue(_) => {} + StmtKind::ExprStmt(expr) => audit_expr(expr, errors), + StmtKind::NamespaceDecl { name: _ } => {} + StmtKind::NamespaceBlock { name: _, body } => audit_stmts(body, errors), + StmtKind::UseDecl { imports: _ } => {} + StmtKind::FunctionDecl { + name, + params, + param_attributes, + variadic: _, + variadic_by_ref: _, + variadic_type, + return_type, + by_ref_return: _, + body, + } => { + reject_reserved_name(errors, span, &Name::unqualified(name)); + for groups in param_attributes { + audit_attribute_groups(groups, errors); + } + audit_params(params, span, errors); + if let Some(variadic_type) = variadic_type { + audit_type(variadic_type, span, errors); + } + if let Some(return_type) = return_type { + audit_type(return_type, span, errors); + } + audit_stmts(body, errors); + } + StmtKind::FunctionVariantGroup { + name: _, + variants: _, + } => {} + StmtKind::FunctionVariantMark { + name: _, + variant: _, + } => {} + StmtKind::Return(value) => { + if let Some(value) = value { + audit_expr(value, errors); + } + } + StmtKind::ConstDecl { name: _, value } => audit_expr(value, errors), + StmtKind::ListUnpack { vars: _, value } => audit_expr(value, errors), + StmtKind::Global { vars: _ } => {} + StmtKind::StaticVar { name: _, init } => audit_expr(init, errors), + StmtKind::ClassDecl { + name: _, + extends: _, + implements: _, + is_abstract: _, + is_final: _, + is_readonly_class: _, + trait_uses: _, + properties, + methods, + constants, + } => { + audit_class_members(properties, methods, constants, span, errors); + } + StmtKind::EnumDecl { + name: _, + backing_type, + cases, + implements: _, + trait_uses: _, + methods, + constants, + } => { + if let Some(backing_type) = backing_type { + audit_type(backing_type, span, errors); + } + for EnumCaseDecl { + name: _, + value, + span: case_span, + attributes, + } in cases + { + let _ = case_span; + audit_attribute_groups(attributes, errors); + if let Some(value) = value { + audit_expr(value, errors); + } + } + audit_class_members(&[], methods, constants, span, errors); + } + StmtKind::PackedClassDecl { name: _, fields: _ } => { + reject( + errors, + span, + "`packed class` is an elephc extension and is not valid PHP", + ); + } + StmtKind::InterfaceDecl { + name: _, + extends: _, + properties, + methods, + constants, + } => { + audit_class_members(properties, methods, constants, span, errors); + } + StmtKind::TraitDecl { + name: _, + trait_uses: _, + properties, + methods, + constants, + } => { + audit_class_members(properties, methods, constants, span, errors); + } + StmtKind::PropertyAssign { + object, + property: _, + value, + } => { + audit_expr(object, errors); + audit_expr(value, errors); + } + StmtKind::StaticPropertyAssign { + receiver: _, + property: _, + value, + } => audit_expr(value, errors), + StmtKind::StaticPropertyArrayPush { + receiver: _, + property: _, + value, + } => audit_expr(value, errors), + StmtKind::StaticPropertyArrayAssign { + receiver: _, + property: _, + index, + value, + } => { + audit_expr(index, errors); + audit_expr(value, errors); + } + StmtKind::PropertyArrayPush { + object, + property: _, + value, + } => { + audit_expr(object, errors); + audit_expr(value, errors); + } + StmtKind::PropertyArrayAssign { + object, + property: _, + index, + value, + } => { + audit_expr(object, errors); + audit_expr(index, errors); + audit_expr(value, errors); + } + StmtKind::ExternFunctionDecl { + name: _, + params: _, + return_type: _, + library: _, + } + | StmtKind::ExternClassDecl { name: _, fields: _ } + | StmtKind::ExternGlobalDecl { name: _, c_type: _ } => { + reject( + errors, + span, + "`extern` declarations are an elephc extension and are not valid PHP", + ); + } + } +} + +/// Audits the shared class-like member lists (properties, methods, constants). +fn audit_class_members( + properties: &[ClassProperty], + methods: &[ClassMethod], + constants: &[ClassConst], + span: Span, + errors: &mut Vec, +) { + for property in properties { + audit_attribute_groups(&property.attributes, errors); + if let Some(type_expr) = &property.type_expr { + audit_type(type_expr, property.span, errors); + } + if let Some(default) = &property.default { + audit_expr(default, errors); + } + } + for method in methods { + audit_attribute_groups(&method.attributes, errors); + for groups in &method.param_attributes { + audit_attribute_groups(groups, errors); + } + audit_params(&method.params, method.span, errors); + if let Some(variadic_type) = &method.variadic_type { + audit_type(variadic_type, method.span, errors); + } + if let Some(return_type) = &method.return_type { + audit_type(return_type, method.span, errors); + } + audit_stmts(&method.body, errors); + } + for constant in constants { + audit_attribute_groups(&constant.attributes, errors); + if let Some(type_expr) = &constant.type_expr { + audit_type(type_expr, constant.span, errors); + } + audit_expr(&constant.value, errors); + let _ = span; + } +} + +/// Audits a function/method/closure parameter list: type annotations and defaults. +fn audit_params( + params: &[(String, Option, Option, bool)], + span: Span, + errors: &mut Vec, +) { + for (_, type_ann, default, _) in params { + if let Some(type_expr) = type_ann { + audit_type(type_expr, span, errors); + } + if let Some(default) = default { + audit_expr(default, errors); + } + } +} + +/// Audits an argument list. +fn audit_exprs(exprs: &[Expr], errors: &mut Vec) { + for expr in exprs { + audit_expr(expr, errors); + } +} + +/// Audits one expression: rejects extension expression forms and reserved +/// `__elephc_*` call targets, recursing into every operand. +fn audit_expr(expr: &Expr, errors: &mut Vec) { + let span = expr.span; + match &expr.kind { + ExprKind::StringLiteral(_) + | ExprKind::IntLiteral(_) + | ExprKind::FloatLiteral(_) + | ExprKind::Variable(_) + | ExprKind::BoolLiteral(_) + | ExprKind::Null + | ExprKind::PreIncrement(_) + | ExprKind::PostIncrement(_) + | ExprKind::PreDecrement(_) + | ExprKind::PostDecrement(_) + | ExprKind::ConstRef(_) + | ExprKind::This + | ExprKind::ClassConstant { receiver: _ } + | ExprKind::ScopedConstantAccess { + receiver: _, + name: _, + } + | ExprKind::MagicConstant(_) => {} + ExprKind::BinaryOp { left, op: _, right } => { + audit_expr(left, errors); + audit_expr(right, errors); + } + ExprKind::InstanceOf { value, target } => { + audit_expr(value, errors); + match target { + InstanceOfTarget::Name(_) => {} + InstanceOfTarget::Expr(target_expr) => audit_expr(target_expr, errors), + } + } + ExprKind::Negate(inner) + | ExprKind::Not(inner) + | ExprKind::BitNot(inner) + | ExprKind::Throw(inner) + | ExprKind::ErrorSuppress(inner) + | ExprKind::Print(inner) + | ExprKind::Spread(inner) + | ExprKind::Clone(inner) + | ExprKind::YieldFrom(inner) => audit_expr(inner, errors), + ExprKind::NullCoalesce { value, default } => { + audit_expr(value, errors); + audit_expr(default, errors); + } + ExprKind::Pipe { value, callable } => { + audit_expr(value, errors); + audit_expr(callable, errors); + } + ExprKind::Assignment { + target, + value, + result_target, + prelude, + conditional_value_temp: _, + } => { + audit_expr(target, errors); + audit_expr(value, errors); + if let Some(result_target) = result_target { + audit_expr(result_target, errors); + } + audit_stmts(prelude, errors); + } + ExprKind::FunctionCall { name, args } => { + reject_reserved_name(errors, span, name); + audit_exprs(args, errors); + } + ExprKind::ArrayLiteral(items) => audit_exprs(items, errors), + ExprKind::ArrayLiteralAssoc(pairs) => { + for (key, value) in pairs { + audit_expr(key, errors); + audit_expr(value, errors); + } + } + ExprKind::Match { + subject, + arms, + default, + } => { + audit_expr(subject, errors); + for (conditions, arm_value) in arms { + audit_exprs(conditions, errors); + audit_expr(arm_value, errors); + } + if let Some(default) = default { + audit_expr(default, errors); + } + } + ExprKind::ArrayAccess { array, index } => { + audit_expr(array, errors); + audit_expr(index, errors); + } + ExprKind::Ternary { + condition, + then_expr, + else_expr, + } => { + audit_expr(condition, errors); + audit_expr(then_expr, errors); + audit_expr(else_expr, errors); + } + ExprKind::ShortTernary { value, default } => { + audit_expr(value, errors); + audit_expr(default, errors); + } + ExprKind::Cast { target: _, expr } => audit_expr(expr, errors), + ExprKind::Closure { + params, + variadic: _, + variadic_by_ref: _, + variadic_type, + return_type, + body, + is_arrow: _, + is_static: _, + by_ref_return: _, + captures: _, + capture_refs: _, + } => { + audit_params(params, span, errors); + if let Some(variadic_type) = variadic_type { + audit_type(variadic_type, span, errors); + } + if let Some(return_type) = return_type { + audit_type(return_type, span, errors); + } + audit_stmts(body, errors); + } + ExprKind::NamedArg { name: _, value } => audit_expr(value, errors), + ExprKind::IncludeValue { + path, + once: _, + required: _, + } => audit_expr(path, errors), + ExprKind::ClosureCall { var: _, args } => audit_exprs(args, errors), + ExprKind::ExprCall { callee, args } => { + audit_expr(callee, errors); + audit_exprs(args, errors); + } + ExprKind::NewObject { + class_name: _, + args, + } => audit_exprs(args, errors), + ExprKind::NewDynamic { name_expr, args } => { + audit_expr(name_expr, errors); + audit_exprs(args, errors); + } + ExprKind::NewDynamicObject { + class_name, + fallback_class: _, + required_parent: _, + args, + } => { + audit_expr(class_name, errors); + audit_exprs(args, errors); + } + ExprKind::PropertyAccess { + object, + property: _, + } + | ExprKind::NullsafePropertyAccess { + object, + property: _, + } => audit_expr(object, errors), + ExprKind::DynamicPropertyAccess { object, property } + | ExprKind::NullsafeDynamicPropertyAccess { object, property } => { + audit_expr(object, errors); + audit_expr(property, errors); + } + ExprKind::StaticPropertyAccess { + receiver: _, + property: _, + } => {} + ExprKind::MethodCall { + object, + method: _, + args, + } + | ExprKind::NullsafeMethodCall { + object, + method: _, + args, + } => { + audit_expr(object, errors); + audit_exprs(args, errors); + } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + audit_expr(object, errors); + audit_expr(method, errors); + audit_exprs(args, errors); + } + ExprKind::StaticMethodCall { + receiver: _, + method: _, + args, + } => audit_exprs(args, errors), + ExprKind::FirstClassCallable(target) => match target { + CallableTarget::Function(name) => reject_reserved_name(errors, span, name), + CallableTarget::StaticMethod { + receiver: _, + method: _, + } => {} + CallableTarget::Method { object, method: _ } => audit_expr(object, errors), + }, + ExprKind::PtrCast { + target_type: _, + expr, + } => { + reject( + errors, + span, + "`ptr_cast` is an elephc extension and is not valid PHP", + ); + audit_expr(expr, errors); + } + ExprKind::BufferNew { element_type, len } => { + reject( + errors, + span, + "`buffer_new` is an elephc extension and is not valid PHP", + ); + audit_type(element_type, span, errors); + audit_expr(len, errors); + } + ExprKind::NewScopedObject { receiver: _, args } => audit_exprs(args, errors), + ExprKind::Yield { key, value } => { + if let Some(key) = key { + audit_expr(key, errors); + } + if let Some(value) = value { + audit_expr(value, errors); + } + } + } +} + +/// Audits a type annotation: rejects `ptr`/`buffer` and recurses through +/// nullable, union, intersection, and array element types. +fn audit_type(type_expr: &TypeExpr, span: Span, errors: &mut Vec) { + match type_expr { + TypeExpr::Int + | TypeExpr::Float + | TypeExpr::Bool + | TypeExpr::False + | TypeExpr::Str + | TypeExpr::Void + | TypeExpr::Never + | TypeExpr::Iterable + | TypeExpr::Named(_) => {} + TypeExpr::Array(inner) => audit_type(inner, span, errors), + TypeExpr::Ptr(_) => { + reject( + errors, + span, + "`ptr` types are an elephc extension and are not valid PHP", + ); + } + TypeExpr::Buffer(inner) => { + reject( + errors, + span, + "`buffer` types are an elephc extension and are not valid PHP", + ); + audit_type(inner, span, errors); + } + TypeExpr::Nullable(inner) => audit_type(inner, span, errors), + TypeExpr::Union(members) | TypeExpr::Intersection(members) => { + for member in members { + audit_type(member, span, errors); + } + } + } +} diff --git a/src/types/checker/builtins/catalog.rs b/src/types/checker/builtins/catalog.rs index de98768dc2..90d421935d 100644 --- a/src/types/checker/builtins/catalog.rs +++ b/src/types/checker/builtins/catalog.rs @@ -14,8 +14,12 @@ //! hidden from `function_exists()` and first-class callable surfaces. const SUPPORTED_BUILTIN_FUNCTIONS: &[&str] = &[ - "buffer_free", - "buffer_len", + // `buffer_new` is a catalog-name-only entry: `buffer_new(len)` is parsed as + // dedicated syntax (`ExprKind::BufferNew`), so the name never dispatches as a + // builtin call; it is listed here for `function_exists`, case-insensitive + // lookup, and redeclaration checks. `buffer_len`/`buffer_free` live in the + // registry (`src/builtins/pointers/`). Like them, it is an elephc extension + // hidden by `--strict-php`. "buffer_new", "die", "empty", @@ -47,14 +51,34 @@ fn is_supported_builtin_function_exact(name: &str) -> bool { || LANGUAGE_CONSTRUCT_FUNCTIONS.contains(&name) } -/// Returns the union of PHP-visible supported builtin function names from the -/// legacy static list and the builtin registry. +/// Returns true when `--strict-php` hides the (lowercase) name from user programs. /// -/// Registry entries flagged as `internal` are excluded, mirroring the semantics -/// of `is_php_visible_builtin_function`. Names present in both sources appear -/// exactly once. With an empty registry this returns the legacy list unchanged, -/// so behavior is preserved while the registry is empty. -pub(crate) fn supported_builtin_function_names() -> Vec<&'static str> { +/// Extension builtins have no PHP equivalent, so strict mode makes them behave +/// as if they did not exist: calls fall through to user-function resolution and +/// the standard undefined-function diagnostics, redeclaration checks accept user +/// functions with these names, and `function_exists()` reports `false`. +/// `internal: true` builtins are never hidden — injected compiler preludes call +/// them and they are already invisible to user programs. `buffer_new` is the one +/// catalog-name-only extension (its call form is dedicated syntax). +pub(crate) fn strict_php_hidden_builtin(canonical: &str) -> bool { + if !crate::strict_php::is_enabled() { + return false; + } + if canonical == "buffer_new" { + return true; + } + crate::builtins::registry::lookup(canonical) + .map(|def| def.spec.extension && !def.spec.internal) + .unwrap_or(false) +} + +/// Returns the union of PHP-visible builtin names from the legacy static list +/// and the builtin registry, WITHOUT the strict-PHP filter. +/// +/// This is the raw catalog snapshot for metadata consumers (parity gates, docs +/// exporters) that memoize the result and must be independent of the thread's +/// strict-mode state. Compilation surfaces use `supported_builtin_function_names`. +pub(crate) fn all_supported_builtin_function_names() -> Vec<&'static str> { let mut result: Vec<&'static str> = SUPPORTED_BUILTIN_FUNCTIONS.to_vec(); for name in crate::builtins::registry::names() { let def = match crate::builtins::registry::lookup(name) { @@ -73,13 +97,32 @@ pub(crate) fn supported_builtin_function_names() -> Vec<&'static str> { result } +/// Returns the union of PHP-visible supported builtin function names from the +/// legacy static list and the builtin registry. +/// +/// Registry entries flagged as `internal` are excluded, mirroring the semantics +/// of `is_php_visible_builtin_function`. Names present in both sources appear +/// exactly once. With an empty registry this returns the legacy list unchanged, +/// so behavior is preserved while the registry is empty. Under `--strict-php`, +/// extension builtins are excluded entirely. +pub(crate) fn supported_builtin_function_names() -> Vec<&'static str> { + all_supported_builtin_function_names() + .into_iter() + .filter(|name| !strict_php_hidden_builtin(&name.to_ascii_lowercase())) + .collect() +} + /// Converts a function name to lowercase and returns it if it is a supported builtin. /// /// Returns `None` if the name is not in either the legacy catalog or the builtin -/// registry. Implements PHP's case-insensitive builtin lookup. The legacy static -/// list is consulted first; the registry is the fallback. +/// registry, or if `--strict-php` hides it (extension builtins). Implements PHP's +/// case-insensitive builtin lookup. The legacy static list is consulted first; +/// the registry is the fallback. pub(crate) fn canonical_builtin_function_name(name: &str) -> Option { let canonical = name.to_ascii_lowercase(); + if strict_php_hidden_builtin(&canonical) { + return None; + } if is_supported_builtin_function_exact(&canonical) || crate::builtins::registry::is_supported(&canonical) { @@ -92,9 +135,13 @@ pub(crate) fn canonical_builtin_function_name(name: &str) -> Option { /// Returns true only for PHP-visible builtin functions (non-internal builtins). /// /// Checks both the legacy static list and the builtin registry. Registry entries -/// flagged as `internal` are excluded from the PHP-visible set. +/// flagged as `internal` are excluded from the PHP-visible set, and `--strict-php` +/// additionally excludes extension builtins. pub(crate) fn is_php_visible_builtin_function(name: &str) -> bool { let canonical = name.to_ascii_lowercase(); + if strict_php_hidden_builtin(&canonical) { + return false; + } SUPPORTED_BUILTIN_FUNCTIONS.contains(&canonical.as_str()) || crate::builtins::registry::lookup(&canonical) .map(|def| !def.spec.internal) @@ -159,4 +206,77 @@ mod tests { "supported_builtin_function_names must include non-internal registry entries" ); } + + /// 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 + /// guard-restored on panic), so this cannot affect parallel tests. + #[test] + fn strict_mode_hides_extension_builtins_from_catalog() { + let _guard = crate::strict_php::scoped_enable(); + assert!( + canonical_builtin_function_name("ptr_get").is_none(), + "strict must hide ptr_get" + ); + assert!( + !is_php_visible_builtin_function("ptr_get"), + "strict must hide ptr_get from PHP visibility" + ); + assert!( + canonical_builtin_function_name("buffer_new").is_none(), + "strict must hide buffer_new" + ); + assert!( + !is_php_visible_builtin_function("buffer_new"), + "strict must hide buffer_new from PHP visibility" + ); + let names = supported_builtin_function_names(); + assert!( + !names.contains(&"ptr_get") && !names.contains(&"buffer_new"), + "strict must drop extension names from the supported set" + ); + } + + /// Verifies strict mode keeps genuine PHP builtins and internal prelude + /// aliases resolvable: hiding either would break normal programs or + /// compiler-injected prelude code. + #[test] + fn strict_mode_keeps_php_builtins_and_internal_aliases() { + let _guard = crate::strict_php::scoped_enable(); + assert_eq!( + canonical_builtin_function_name("strlen"), + Some("strlen".to_string()) + ); + assert_eq!( + canonical_builtin_function_name("is_real"), + Some("is_real".to_string()), + "is_real is treated as PHP for strict purposes" + ); + assert!( + canonical_builtin_function_name("__elephc_ptr_read_string").is_some(), + "internal prelude aliases must stay resolvable in strict mode" + ); + } + + /// Verifies the unfiltered name set ignores strict mode entirely: metadata + /// consumers (parity gates, docs exporters) memoize this snapshot and must + /// never observe a strict-filtered view. + #[test] + fn unfiltered_name_set_ignores_strict_mode() { + let _guard = crate::strict_php::scoped_enable(); + let names = all_supported_builtin_function_names(); + assert!(names.contains(&"ptr_get")); + assert!(names.contains(&"buffer_new")); + assert!(names.contains(&"strlen")); + } + + /// Verifies extension builtins remain fully visible without strict mode, so + /// the filter cannot regress the default compilation mode. + #[test] + fn non_strict_keeps_extension_builtins_visible() { + assert!(canonical_builtin_function_name("ptr_get").is_some()); + assert!(is_php_visible_builtin_function("ptr_get")); + assert!(canonical_builtin_function_name("buffer_new").is_some()); + assert!(supported_builtin_function_names().contains(&"buffer_new")); + } } diff --git a/src/types/checker/builtins/mod.rs b/src/types/checker/builtins/mod.rs index c4976ce7e6..58bdb2ec4e 100644 --- a/src/types/checker/builtins/mod.rs +++ b/src/types/checker/builtins/mod.rs @@ -10,7 +10,7 @@ pub(crate) mod arrays; mod callables; -mod catalog; +pub(crate) mod catalog; pub(crate) mod io; mod numeric; pub(crate) mod spl; @@ -23,7 +23,8 @@ use super::Checker; pub(crate) use catalog::{ canonical_builtin_function_name, is_php_visible_builtin_function, - is_supported_builtin_function, supported_builtin_function_names, + is_supported_builtin_function, strict_php_hidden_builtin, + supported_builtin_function_names, }; pub(crate) use callables::{ array_element_type, array_filter_callback_dummy_args, callback_supports_complex_descriptor_env, @@ -66,6 +67,14 @@ 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('\\')); + // `--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 + // run before argument normalization so the hidden builtin's signature is + // never applied to the call. + if catalog::strict_php_hidden_builtin(&builtin_key) { + return Ok(None); + } let is_lazy_construct = matches!(builtin_key.as_str(), "isset" | "unset"); let normalized_args; let args = if let Some(sig) = diff --git a/src/types/checker/builtins/numeric.rs b/src/types/checker/builtins/numeric.rs index 5bf8f2cfbb..c4a06b4cf0 100644 --- a/src/types/checker/builtins/numeric.rs +++ b/src/types/checker/builtins/numeric.rs @@ -104,61 +104,6 @@ pub(super) fn check_builtin( } Ok(Some(PhpType::Void)) } - "buffer_len" => { - if args.len() != 1 { - return Err(CompileError::new(span, "buffer_len() takes exactly 1 argument")); - } - let ty = checker.infer_type(&args[0], env)?; - if !matches!(ty, PhpType::Buffer(_)) { - return Err(CompileError::new( - span, - "buffer_len() argument must be buffer", - )); - } - Ok(Some(PhpType::Int)) - } - "buffer_free" => { - if args.len() != 1 { - return Err(CompileError::new(span, "buffer_free() takes exactly 1 argument")); - } - match &args[0].kind { - ExprKind::Variable(name) => { - if checker.current_class.is_some() && name == "this" { - return Err(CompileError::new(span, "buffer_free() cannot free $this")); - } - if checker.active_ref_params.contains(name) - || checker.active_globals.contains(name) - || checker.active_statics.contains(name) - { - return Err(CompileError::new( - span, - "buffer_free() argument must be a local variable", - )); - } - } - _ => { - let ty = checker.infer_type(&args[0], env)?; - if !matches!(ty, PhpType::Buffer(_)) { - return Err(CompileError::new( - span, - "buffer_free() argument must be buffer", - )); - } - return Err(CompileError::new( - span, - "buffer_free() argument must be a local variable", - )); - } - } - let ty = checker.infer_type(&args[0], env)?; - if !matches!(ty, PhpType::Buffer(_)) { - return Err(CompileError::new( - span, - "buffer_free() argument must be buffer", - )); - } - Ok(Some(PhpType::Void)) - } _ => Ok(None), } } diff --git a/src/types/checker/functions/resolution/mod.rs b/src/types/checker/functions/resolution/mod.rs index c9481ad9df..b42cadfa0d 100644 --- a/src/types/checker/functions/resolution/mod.rs +++ b/src/types/checker/functions/resolution/mod.rs @@ -79,6 +79,20 @@ impl Checker { }; return CompileError::new(span, &message); } + // Under `--strict-php`, a name that would resolve to an extension builtin + // lands here as undefined. Name the disabled extension so users understand + // why a working non-strict program stopped compiling. + let bare = name.rsplit('\\').next().unwrap_or(name); + let key = crate::names::php_symbol_key(bare); + if crate::types::checker::builtins::strict_php_hidden_builtin(&key) { + return CompileError::new( + span, + &format!( + "Undefined function: {} ({}() exists as an elephc extension; it is disabled by --strict-php)", + name, key + ), + ); + } CompileError::new(span, &format!("Undefined function: {}", name)) } diff --git a/src/web_prelude.rs b/src/web_prelude.rs index 71db74ff65..5bf00e73a2 100644 --- a/src/web_prelude.rs +++ b/src/web_prelude.rs @@ -63,12 +63,12 @@ impl PhpVersion { /// The PHP source prepended under `--web`. Phase 2 Task 2: extern declarations; /// Task 5: $_SERVER; Task 6: $_GET parsed from the query string; Task 7: $_POST /// parsed from a `application/x-www-form-urlencoded` body (read binary-safe via -/// `ptr_read_string(elephc_web_body_ptr(), elephc_web_body_len())`). The query/ +/// `__elephc_ptr_read_string(elephc_web_body_ptr(), elephc_web_body_len())`). The query/ /// body parsers are built inline (element-by-element into the superglobal), /// mirroring the $_SERVER pattern, to stay within the type checker's proven /// capabilities (a helper function returning a freshly-built assoc array trips /// return-type inference / union widening). -const WEB_PRELUDE_SRC: &str = r#" 0) { - $__elephc_body = ptr_read_string(elephc_web_body_ptr(), $__elephc_body_len); + $__elephc_body = __elephc_ptr_read_string(elephc_web_body_ptr(), $__elephc_body_len); } if ($__elephc_body !== '') { $__elephc_ppairs = explode('&', $__elephc_body); @@ -266,7 +266,7 @@ if (strpos(strtoupper($__elephc_ct), 'MULTIPART/FORM-DATA') !== false) { $__elephc_mpv_len = elephc_web_multipart_value_len($__elephc_mpi); $__elephc_mpv = ''; if ($__elephc_mpv_len > 0) { - $__elephc_mpv = ptr_read_string( + $__elephc_mpv = __elephc_ptr_read_string( elephc_web_multipart_value_ptr($__elephc_mpi), $__elephc_mpv_len ); @@ -352,15 +352,16 @@ interface SessionUpdateTimestampHandlerInterface { function __elephc_session_stage_bytes(string $data): ptr { $__elephc_sb_len = strlen($data); $__elephc_sb_ptr = elephc_web_session_data_stage($__elephc_sb_len); - if ($__elephc_sb_len > 0) { ptr_write_string($__elephc_sb_ptr, $data); } + if ($__elephc_sb_len > 0) { __elephc_ptr_write_string($__elephc_sb_ptr, $data); } return $__elephc_sb_ptr; } function __elephc_session_copy_bytes(ptr $data, int $len): string { if ($len === 0) { return ''; } - // `ptr_read_string()` is borrowed. Materialize it through `str_repeat()` so - // the returned PHP string owns compiler-heap storage and cannot be changed - // by the next bridge key/value publication during session decoding. - return str_repeat(ptr_read_string($data, $len), 1); + // `__elephc_ptr_read_string()` returns a borrowed view. Materialize it + // through `str_repeat()` so the returned PHP string owns compiler-heap + // storage and cannot be changed by the next bridge key/value publication + // during session decoding. + return str_repeat(__elephc_ptr_read_string($data, $len), 1); } function __elephc_session_read_file(string $id, string $save_path, int $read_and_close): string { $__elephc_rf_ptr = elephc_web_session_read_bytes($id, $save_path, $read_and_close); @@ -1684,7 +1685,7 @@ if (elephc_web_session_get_auto_start() === 1) { __elephc_session_start_core(0); /// uncaught exception sets a 500 status instead of crashing the worker (the /// process would otherwise die and the master would respawn it, dropping the /// connection). The `0;` placeholder body is replaced with the real statements. -const WEB_WRAP_SRC: &str = +pub(crate) const WEB_WRAP_SRC: &str = ">(); + let expected = elephc::builtin_metadata::extension_builtin_names() + .iter() + .copied() + .filter(|name| !static_only.contains(name)) + .collect::>(); + let eval_extensions = elephc_magician::builtin_metadata::extension_builtin_names() + .iter() + .copied() + .collect::>(); + + assert_eq!( + eval_extensions, expected, + "eval extension set must match the compiler's extension set minus static-only builtins" + ); +} diff --git a/tests/codegen/mod.rs b/tests/codegen/mod.rs index 3bb13b0a29..696931a7c2 100644 --- a/tests/codegen/mod.rs +++ b/tests/codegen/mod.rs @@ -15,6 +15,7 @@ mod namespaces; mod null_sentinel; mod case_insensitive_symbols; mod cli; +mod strict_php; mod benchmarks; mod echo_vars; mod eval; diff --git a/tests/codegen/strict_php.rs b/tests/codegen/strict_php.rs new file mode 100644 index 0000000000..edc39b6ee4 --- /dev/null +++ b/tests/codegen/strict_php.rs @@ -0,0 +1,316 @@ +//! Purpose: +//! End-to-end CLI tests for `--strict-php`: plain PHP compiles and runs +//! identically, user code may shadow extension builtin names, `function_exists` +//! reports extensions as missing, extension constructs fail with the strict +//! diagnostics, and injected preludes keep working. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Every test spawns the real `elephc` binary (CARGO_BIN_EXE_elephc) so the +//! whole pipeline — CLI parsing, thread-local strict state, audit hooks, +//! catalog filtering, codegen — is exercised together. + +use crate::support::*; + +/// Compiles `source` through the CLI with `--strict-php`, runs the binary, and +/// returns its stdout. Panics if compilation or the run fails. +fn compile_strict_cli_and_run(source: &str) -> String { + let dir = make_cli_test_dir("elephc_cli_strict"); + let php_path = dir.join("main.php"); + fs::write(&php_path, source).unwrap(); + + let compile_out = elephc_cli_command(&dir) + .arg("--strict-php") + .arg(&php_path) + .output() + .expect("failed to run elephc CLI"); + assert!( + compile_out.status.success(), + "elephc --strict-php failed: {}", + String::from_utf8_lossy(&compile_out.stderr) + ); + + let bin_path = dir.join("main"); + let output = run_binary(&bin_path, &dir); + assert!( + output.status.success(), + "strict-compiled binary exited with error" + ); + + let _ = fs::remove_dir_all(&dir); + String::from_utf8(output.stdout).unwrap() +} + +/// Compiles `source` through the CLI with the given extra flags, expecting the +/// compiler to fail; returns its stderr. +fn compile_cli_expect_error(source: &str, flags: &[&str]) -> String { + let dir = make_cli_test_dir("elephc_cli_strict_err"); + let php_path = dir.join("main.php"); + fs::write(&php_path, source).unwrap(); + + let compile_out = elephc_cli_command(&dir) + .args(flags) + .arg(&php_path) + .output() + .expect("failed to run elephc CLI"); + assert!( + !compile_out.status.success(), + "expected elephc to fail with flags {flags:?}, but it succeeded" + ); + + let _ = fs::remove_dir_all(&dir); + String::from_utf8_lossy(&compile_out.stderr).into_owned() +} + +/// Verifies a plain PHP program produces the same output with and without +/// `--strict-php`: strict mode must not change the semantics of PHP code. +#[test] +fn test_strict_php_plain_program_runs_identically() { + let source = r#" 1"), + "unexpected output: {out}", + ); +} + +/// Verifies `--check --strict-php` reports strict violations without emitting +/// artifacts, so strict mode composes with check-only runs. +#[test] +fn test_strict_php_composes_with_check_mode() { + let stderr = compile_cli_expect_error( + " String { + let dir = make_cli_test_dir("elephc_cli_strict_rt"); + let php_path = dir.join("main.php"); + fs::write(&php_path, source).unwrap(); + + let compile_out = elephc_cli_command(&dir) + .arg("--strict-php") + .arg(&php_path) + .output() + .expect("failed to run elephc CLI"); + assert!( + compile_out.status.success(), + "elephc --strict-php failed: {}", + String::from_utf8_lossy(&compile_out.stderr) + ); + + let bin_path = dir.join("main"); + let output = run_binary(&bin_path, &dir); + assert!( + !output.status.success(), + "expected the strict binary to fail at runtime" + ); + + let _ = fs::remove_dir_all(&dir); + format!( + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ) +} + +/// Verifies dynamic (runtime-string) eval hides extension builtins under +/// strict mode: the call behaves like any unknown function in eval'd code. +#[test] +fn test_strict_php_dynamic_eval_hides_extension_builtins() { + let out = compile_strict_cli_and_run_expect_failure( + r#" Result<(), String> { + let _guard = elephc::strict_php::scoped_enable(); + check_source(src) +} + +/// Asserts that `src` fails under strict mode with a message containing `expected_substr`. +fn expect_strict_error(src: &str, expected_substr: &str) { + match check_source_strict(src) { + Ok(_) => panic!( + "Expected strict-php error containing '{}', but got Ok", + expected_substr + ), + Err(msg) => { + assert!( + msg.contains(expected_substr), + "Error '{}' doesn't contain '{}'", + msg, + expected_substr, + ); + } + } +} + +/// Verifies a call to an extension builtin is an undefined function under strict +/// mode, exactly as it would be under the PHP interpreter. +#[test] +fn test_strict_error_extension_builtin_call_is_undefined() { + expect_strict_error(" Vec { + let tokens = tokenize(src).expect("audit fixtures must tokenize"); + let ast = parse(&tokens).expect("audit fixtures must parse"); + elephc::strict_php::check(&ast) + .into_iter() + .map(|e| e.message) + .collect() +} + +/// Asserts the audit reports exactly one violation containing `expected_substr`. +fn expect_audit_violation(src: &str, expected_substr: &str) { + let messages = strict_audit_messages(src); + assert!( + messages.iter().any(|m| m.contains(expected_substr)), + "Audit messages {messages:?} do not contain '{expected_substr}'", + ); +} + +/// Verifies the audit rejects `ifdef` conditional compilation blocks. +#[test] +fn test_audit_rejects_ifdef() { + expect_audit_violation( + "(...)` expressions. +#[test] +fn test_audit_rejects_ptr_cast() { + expect_audit_violation( + "($x);", + "`ptr_cast` is an elephc extension", + ); +} + +/// Verifies the audit rejects `buffer_new(...)` allocations. +#[test] +fn test_audit_rejects_buffer_new() { + expect_audit_violation( + " $b = buffer_new(4);", + "`buffer_new` is an elephc extension", + ); +} + +/// Verifies the audit rejects typed local variable declarations, which PHP +/// does not support for any type. +#[test] +fn test_audit_rejects_typed_local_declaration() { + expect_audit_violation( + "` types in function return positions. +#[test] +fn test_audit_rejects_buffer_return_type() { + expect_audit_violation( + " { return buffer_new(1); }", + "`buffer` types are an elephc extension", + ); +} + +/// Verifies the audit rejects `ptr` property types nested inside classes. +#[test] +fn test_audit_rejects_ptr_property_type() { + expect_audit_violation( + "(4))] function f(): void {} echo \"ok\";", + "`buffer_new` is an elephc extension", + ); +} + +/// Verifies extension expressions inside parameter attribute arguments are rejected. +#[test] +fn test_audit_rejects_extension_in_param_attribute_args() { + expect_audit_violation( + "(2))] int $x): void {}", + "`buffer_new` is an elephc extension", + ); +} + +/// Verifies extension expressions inside property attribute arguments are rejected. +#[test] +fn test_audit_rejects_extension_in_property_attribute_args() { + expect_audit_violation( + "(1))] public int $x = 0; }", + "`buffer_new` is an elephc extension", + ); +} + +/// Verifies extension expressions inside method attribute arguments are rejected. +#[test] +fn test_audit_rejects_extension_in_method_attribute_args() { + expect_audit_violation( + "(1))] public function m(): void {} }", + "`buffer_new` is an elephc extension", + ); +} + +/// Verifies compiler-reserved `__elephc_*` calls inside attribute arguments are rejected. +#[test] +fn test_audit_rejects_reserved_call_in_attribute_args() { + expect_audit_violation( + "= 2, + "expected at least 2 violations, got {messages:?}", + ); +} + +/// Writes `files` to a temp project and runs the resolver (which parses +/// includes) with strict-PHP mode enabled, returning the resolver outcome. +fn resolve_files_strict( + files: &[(&str, &str)], + main_file: &str, +) -> Result<(), elephc::errors::CompileError> { + let id = TEST_PROJECT_ID.fetch_add(1, Ordering::SeqCst); + let dir = + std::env::temp_dir().join(format!("elephc_strict_test_{}_{}", std::process::id(), id)); + fs::create_dir_all(&dir).unwrap(); + for (path, content) in files { + let full_path = dir.join(path); + if let Some(parent) = full_path.parent() { + fs::create_dir_all(parent).unwrap(); + } + fs::write(&full_path, content).unwrap(); + } + + let php_path = dir.join(main_file); + let source = fs::read_to_string(&php_path).unwrap(); + let base_dir = php_path.parent().unwrap(); + + let result = { + let _guard = elephc::strict_php::scoped_enable(); + (|| -> Result<(), elephc::errors::CompileError> { + let tokens = tokenize(&source)?; + let ast = parse(&tokens)?; + let _ = elephc::resolver::resolve(ast, base_dir)?; + Ok(()) + })() + }; + + let _ = fs::remove_dir_all(&dir); + result +} + +/// Verifies an `extern` block inside an included file is rejected under strict +/// mode: the resolver audits every included user file at parse time. +#[test] +fn test_strict_rejects_extern_inside_include() { + let err = resolve_files_strict( + &[ + ("main.php", "name = $name; } + public function greet(): string { return 'hi ' . $this->name; } + } + $g = new Greeter('world'); + echo $g->greet(), fib(10); + foreach ([1, 2, 3] as $k => $v) { echo $k + $v; } + $f = fn(int $x): int => $x * 2; + echo $f(21), strlen('abc'), PHP_EOL;", + ); + assert!(messages.is_empty(), "expected no violations, got {messages:?}"); +} diff --git a/tools/gen_builtins.rs b/tools/gen_builtins.rs index 2aecf86836..944818fc5c 100644 --- a/tools/gen_builtins.rs +++ b/tools/gen_builtins.rs @@ -129,10 +129,16 @@ fn append_eval_only_records(records: &mut Vec, include_internal: bool) { }) }) .collect(); + // Appended records have no static `builtin!` spec, so the extension + // classification comes from the eval registry's derived set (this is + // how the catalog-name-only `buffer_new` gets flagged). + let extension = elephc_magician::builtin_metadata::extension_builtin_names() + .contains(&meta.name.as_str()); records.push(json!({ "name": meta.name, "area": meta.area, "internal": internal, + "extension": extension, "params": params, "variadic": meta.variadic, "returns": "mixed",